diff options
547 files changed, 19512 insertions, 7582 deletions
diff --git a/Android.bp b/Android.bp index 0315c12f95a1..0a1456514010 100644 --- a/Android.bp +++ b/Android.bp @@ -214,6 +214,7 @@ java_library { "android.hardware.radio-V1.5-java", "android.hardware.radio-V1.6-java", "android.hardware.radio.data-V1-java", + "android.hardware.radio.ims-V1-java", "android.hardware.radio.messaging-V1-java", "android.hardware.radio.modem-V1-java", "android.hardware.radio.network-V2-java", diff --git a/apct-tests/perftests/core/Android.bp b/apct-tests/perftests/core/Android.bp index ab20fdbde1e5..98e4f4509b52 100644 --- a/apct-tests/perftests/core/Android.bp +++ b/apct-tests/perftests/core/Android.bp @@ -44,6 +44,8 @@ android_test { "apct-perftests-utils", "collector-device-lib", "compatibility-device-util-axt", + "junit", + "junit-params", "core-tests-support", "guava", ], diff --git a/apct-tests/perftests/core/src/android/libcore/DeepArrayOpsPerfTest.java b/apct-tests/perftests/core/src/android/libcore/DeepArrayOpsPerfTest.java index 3f4f6af7554c..3ebaa4cd0bfa 100644 --- a/apct-tests/perftests/core/src/android/libcore/DeepArrayOpsPerfTest.java +++ b/apct-tests/perftests/core/src/android/libcore/DeepArrayOpsPerfTest.java @@ -20,18 +20,19 @@ import android.perftests.utils.BenchmarkState; import android.perftests.utils.PerfStatusReporter; import android.test.suitebuilder.annotation.LargeTest; -import org.junit.Before; +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; + import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; import java.lang.reflect.Array; import java.lang.reflect.Constructor; import java.util.Arrays; import java.util.Collection; -@RunWith(Parameterized.class) +@RunWith(JUnitParamsRunner.class) @LargeTest public class DeepArrayOpsPerfTest { @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); @@ -39,19 +40,14 @@ public class DeepArrayOpsPerfTest { private Object[] mArray; private Object[] mArray2; - @Parameterized.Parameter(0) - public int mArrayLength; - - @Parameterized.Parameters(name = "mArrayLength({0})") - public static Collection<Object[]> data() { + public static Collection<Object[]> getData() { return Arrays.asList(new Object[][] {{1}, {4}, {16}, {32}, {2048}}); } - @Before - public void setUp() throws Exception { - mArray = new Object[mArrayLength * 14]; - mArray2 = new Object[mArrayLength * 14]; - for (int i = 0; i < mArrayLength; i += 14) { + public void setUp(int arrayLength) throws Exception { + mArray = new Object[arrayLength * 14]; + mArray2 = new Object[arrayLength * 14]; + for (int i = 0; i < arrayLength; i += 14) { mArray[i] = new IntWrapper(i); mArray2[i] = new IntWrapper(i); @@ -99,7 +95,9 @@ public class DeepArrayOpsPerfTest { } @Test - public void deepHashCode() { + @Parameters(method = "getData") + public void deepHashCode(int arrayLength) throws Exception { + setUp(arrayLength); BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { Arrays.deepHashCode(mArray); @@ -107,7 +105,9 @@ public class DeepArrayOpsPerfTest { } @Test - public void deepEquals() { + @Parameters(method = "getData") + public void deepEquals(int arrayLength) throws Exception { + setUp(arrayLength); BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { Arrays.deepEquals(mArray, mArray2); diff --git a/apct-tests/perftests/core/src/android/libcore/SystemArrayCopyPerfTest.java b/apct-tests/perftests/core/src/android/libcore/SystemArrayCopyPerfTest.java index 5aacfc25bfd1..20f1309bd8e6 100644 --- a/apct-tests/perftests/core/src/android/libcore/SystemArrayCopyPerfTest.java +++ b/apct-tests/perftests/core/src/android/libcore/SystemArrayCopyPerfTest.java @@ -20,22 +20,22 @@ import android.perftests.utils.BenchmarkState; import android.perftests.utils.PerfStatusReporter; import android.test.suitebuilder.annotation.LargeTest; +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; + import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameters; import java.util.Arrays; import java.util.Collection; -@RunWith(Parameterized.class) +@RunWith(JUnitParamsRunner.class) @LargeTest public class SystemArrayCopyPerfTest { @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); - @Parameters(name = "arrayLength={0}") - public static Collection<Object[]> data() { + public static Collection<Object[]> getData() { return Arrays.asList( new Object[][] { {2}, {4}, {8}, {16}, {32}, {64}, {128}, {256}, {512}, {1024}, {2048}, {4096}, @@ -43,12 +43,10 @@ public class SystemArrayCopyPerfTest { }); } - @Parameterized.Parameter(0) - public int arrayLength; - // Provides benchmarking for different types of arrays using the arraycopy function. @Test - public void timeSystemCharArrayCopy() { + @Parameters(method = "getData") + public void timeSystemCharArrayCopy(int arrayLength) { final int len = arrayLength; char[] src = new char[len]; char[] dst = new char[len]; @@ -59,7 +57,8 @@ public class SystemArrayCopyPerfTest { } @Test - public void timeSystemByteArrayCopy() { + @Parameters(method = "getData") + public void timeSystemByteArrayCopy(int arrayLength) { final int len = arrayLength; byte[] src = new byte[len]; byte[] dst = new byte[len]; @@ -70,7 +69,8 @@ public class SystemArrayCopyPerfTest { } @Test - public void timeSystemShortArrayCopy() { + @Parameters(method = "getData") + public void timeSystemShortArrayCopy(int arrayLength) { final int len = arrayLength; short[] src = new short[len]; short[] dst = new short[len]; @@ -81,7 +81,8 @@ public class SystemArrayCopyPerfTest { } @Test - public void timeSystemIntArrayCopy() { + @Parameters(method = "getData") + public void timeSystemIntArrayCopy(int arrayLength) { final int len = arrayLength; int[] src = new int[len]; int[] dst = new int[len]; @@ -92,7 +93,8 @@ public class SystemArrayCopyPerfTest { } @Test - public void timeSystemLongArrayCopy() { + @Parameters(method = "getData") + public void timeSystemLongArrayCopy(int arrayLength) { final int len = arrayLength; long[] src = new long[len]; long[] dst = new long[len]; @@ -103,7 +105,8 @@ public class SystemArrayCopyPerfTest { } @Test - public void timeSystemFloatArrayCopy() { + @Parameters(method = "getData") + public void timeSystemFloatArrayCopy(int arrayLength) { final int len = arrayLength; float[] src = new float[len]; float[] dst = new float[len]; @@ -114,7 +117,8 @@ public class SystemArrayCopyPerfTest { } @Test - public void timeSystemDoubleArrayCopy() { + @Parameters(method = "getData") + public void timeSystemDoubleArrayCopy(int arrayLength) { final int len = arrayLength; double[] src = new double[len]; double[] dst = new double[len]; @@ -125,7 +129,8 @@ public class SystemArrayCopyPerfTest { } @Test - public void timeSystemBooleanArrayCopy() { + @Parameters(method = "getData") + public void timeSystemBooleanArrayCopy(int arrayLength) { final int len = arrayLength; boolean[] src = new boolean[len]; boolean[] dst = new boolean[len]; diff --git a/apct-tests/perftests/core/src/android/libcore/XmlSerializePerfTest.java b/apct-tests/perftests/core/src/android/libcore/XmlSerializePerfTest.java index eec0734cffda..b1b594d64324 100644 --- a/apct-tests/perftests/core/src/android/libcore/XmlSerializePerfTest.java +++ b/apct-tests/perftests/core/src/android/libcore/XmlSerializePerfTest.java @@ -20,42 +20,32 @@ import android.perftests.utils.BenchmarkState; import android.perftests.utils.PerfStatusReporter; import android.test.suitebuilder.annotation.LargeTest; -import org.junit.Before; +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; + import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameters; import org.xmlpull.v1.XmlSerializer; import java.io.CharArrayWriter; import java.lang.reflect.Constructor; -import java.util.Arrays; -import java.util.Collection; import java.util.Random; -@RunWith(Parameterized.class) +@RunWith(JUnitParamsRunner.class) @LargeTest public class XmlSerializePerfTest { @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); - @Parameters(name = "mDatasetAsString({0}), mSeed({1})") - public static Collection<Object[]> data() { - return Arrays.asList( - new Object[][] { - {"0.99 0.7 0.7 0.7 0.7 0.7", 854328}, - {"0.999 0.3 0.3 0.95 0.9 0.9", 854328}, - {"0.99 0.7 0.7 0.7 0.7 0.7", 312547}, - {"0.999 0.3 0.3 0.95 0.9 0.9", 312547} - }); + private Object[] getParams() { + return new Object[][] { + new Object[] {"0.99 0.7 0.7 0.7 0.7 0.7", 854328}, + new Object[] {"0.999 0.3 0.3 0.95 0.9 0.9", 854328}, + new Object[] {"0.99 0.7 0.7 0.7 0.7 0.7", 312547}, + new Object[] {"0.999 0.3 0.3 0.95 0.9 0.9", 312547} + }; } - @Parameterized.Parameter(0) - public String mDatasetAsString; - - @Parameterized.Parameter(1) - public int mSeed; - double[] mDataset; private Constructor<? extends XmlSerializer> mKxmlConstructor; private Constructor<? extends XmlSerializer> mFastConstructor; @@ -100,8 +90,7 @@ public class XmlSerializePerfTest { } @SuppressWarnings("unchecked") - @Before - public void setUp() throws Exception { + public void setUp(String datasetAsString) throws Exception { mKxmlConstructor = (Constructor) Class.forName("com.android.org.kxml2.io.KXmlSerializer").getConstructor(); @@ -109,28 +98,32 @@ public class XmlSerializePerfTest { (Constructor) Class.forName("com.android.internal.util.FastXmlSerializer") .getConstructor(); - String[] splitStrings = mDatasetAsString.split(" "); + String[] splitStrings = datasetAsString.split(" "); mDataset = new double[splitStrings.length]; for (int i = 0; i < splitStrings.length; i++) { mDataset[i] = Double.parseDouble(splitStrings[i]); } } - private void internalTimeSerializer(Constructor<? extends XmlSerializer> ctor) + private void internalTimeSerializer(Constructor<? extends XmlSerializer> ctor, int seed) throws Exception { BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { - serializeRandomXml(ctor, mSeed); + serializeRandomXml(ctor, seed); } } @Test - public void timeKxml() throws Exception { - internalTimeSerializer(mKxmlConstructor); + @Parameters(method = "getParams") + public void timeKxml(String datasetAsString, int seed) throws Exception { + setUp(datasetAsString); + internalTimeSerializer(mKxmlConstructor, seed); } @Test - public void timeFast() throws Exception { - internalTimeSerializer(mFastConstructor); + @Parameters(method = "getParams") + public void timeFast(String datasetAsString, int seed) throws Exception { + setUp(datasetAsString); + internalTimeSerializer(mFastConstructor, seed); } } diff --git a/apct-tests/perftests/core/src/android/libcore/ZipFilePerfTest.java b/apct-tests/perftests/core/src/android/libcore/ZipFilePerfTest.java index 31c92ba5e207..3a45d4045d62 100644 --- a/apct-tests/perftests/core/src/android/libcore/ZipFilePerfTest.java +++ b/apct-tests/perftests/core/src/android/libcore/ZipFilePerfTest.java @@ -20,12 +20,12 @@ import android.perftests.utils.BenchmarkState; import android.perftests.utils.PerfStatusReporter; import android.test.suitebuilder.annotation.LargeTest; -import org.junit.Before; +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; + import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameters; import java.io.File; import java.io.FileOutputStream; @@ -38,23 +38,18 @@ import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import java.util.zip.ZipOutputStream; -@RunWith(Parameterized.class) +@RunWith(JUnitParamsRunner.class) @LargeTest public class ZipFilePerfTest { @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); private File mFile; - @Parameters(name = "numEntries={0}") - public static Collection<Object[]> data() { + public static Collection<Object[]> getData() { return Arrays.asList(new Object[][] {{128}, {1024}, {8192}}); } - @Parameterized.Parameter(0) - public int numEntries; - - @Before - public void setUp() throws Exception { + public void setUp(int numEntries) throws Exception { mFile = File.createTempFile(getClass().getName(), ".zip"); mFile.deleteOnExit(); writeEntries(new ZipOutputStream(new FileOutputStream(mFile)), numEntries, 0); @@ -66,7 +61,9 @@ public class ZipFilePerfTest { } @Test - public void timeZipFileOpen() throws Exception { + @Parameters(method = "getData") + public void timeZipFileOpen(int numEntries) throws Exception { + setUp(numEntries); BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { ZipFile zf = new ZipFile(mFile); diff --git a/apct-tests/perftests/core/src/android/libcore/ZipFileReadPerfTest.java b/apct-tests/perftests/core/src/android/libcore/ZipFileReadPerfTest.java index faa96285cefd..2e89518ec9fb 100644 --- a/apct-tests/perftests/core/src/android/libcore/ZipFileReadPerfTest.java +++ b/apct-tests/perftests/core/src/android/libcore/ZipFileReadPerfTest.java @@ -20,12 +20,13 @@ import android.perftests.utils.BenchmarkState; import android.perftests.utils.PerfStatusReporter; import android.test.suitebuilder.annotation.LargeTest; +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; + import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameters; import java.io.File; import java.io.FileOutputStream; @@ -39,21 +40,17 @@ import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import java.util.zip.ZipOutputStream; -@RunWith(Parameterized.class) +@RunWith(JUnitParamsRunner.class) @LargeTest public class ZipFileReadPerfTest { @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); - @Parameters(name = "readBufferSize={0}") - public static Collection<Object[]> data() { + public static Collection<Object[]> getData() { return Arrays.asList(new Object[][] {{1024}, {16384}, {65536}}); } private File mFile; - @Parameterized.Parameter(0) - public int readBufferSize; - @Before public void setUp() throws Exception { mFile = File.createTempFile(getClass().getName(), ".zip"); @@ -90,7 +87,8 @@ public class ZipFileReadPerfTest { } @Test - public void timeZipFileRead() throws Exception { + @Parameters(method = "getData") + public void timeZipFileRead(int readBufferSize) throws Exception { byte[] readBuffer = new byte[readBufferSize]; BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { diff --git a/apct-tests/perftests/core/src/android/libcore/regression/BitSetPerfTest.java b/apct-tests/perftests/core/src/android/libcore/regression/BitSetPerfTest.java index db5462cd69bf..2c0473eda830 100644 --- a/apct-tests/perftests/core/src/android/libcore/regression/BitSetPerfTest.java +++ b/apct-tests/perftests/core/src/android/libcore/regression/BitSetPerfTest.java @@ -20,96 +20,99 @@ import android.perftests.utils.BenchmarkState; import android.perftests.utils.PerfStatusReporter; import android.test.suitebuilder.annotation.LargeTest; -import org.junit.Before; +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; + import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameters; import java.util.Arrays; import java.util.BitSet; import java.util.Collection; -@RunWith(Parameterized.class) +@RunWith(JUnitParamsRunner.class) @LargeTest public class BitSetPerfTest { @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); - @Parameters(name = "mSize={0}") - public static Collection<Object[]> data() { + public static Collection<Object[]> getData() { return Arrays.asList(new Object[][] {{1000}, {10000}}); } - @Parameterized.Parameter(0) - public int mSize; - - private BitSet mBitSet; - - @Before - public void setUp() throws Exception { - mBitSet = new BitSet(mSize); - } - @Test - public void timeIsEmptyTrue() { + @Parameters(method = "getData") + public void timeIsEmptyTrue(int size) { + BitSet bitSet = new BitSet(size); BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { - if (!mBitSet.isEmpty()) throw new RuntimeException(); + if (!bitSet.isEmpty()) throw new RuntimeException(); } } @Test - public void timeIsEmptyFalse() { - mBitSet.set(mBitSet.size() - 1); + @Parameters(method = "getData") + public void timeIsEmptyFalse(int size) { + BitSet bitSet = new BitSet(size); + bitSet.set(bitSet.size() - 1); BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { - if (mBitSet.isEmpty()) throw new RuntimeException(); + if (bitSet.isEmpty()) throw new RuntimeException(); } } @Test - public void timeGet() { + @Parameters(method = "getData") + public void timeGet(int size) { + BitSet bitSet = new BitSet(size); BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); int i = 1; while (state.keepRunning()) { - mBitSet.get(++i % mSize); + bitSet.get(++i % size); } } @Test - public void timeClear() { + @Parameters(method = "getData") + public void timeClear(int size) { + BitSet bitSet = new BitSet(size); BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); int i = 1; while (state.keepRunning()) { - mBitSet.clear(++i % mSize); + bitSet.clear(++i % size); } } @Test - public void timeSet() { + @Parameters(method = "getData") + public void timeSet(int size) { + BitSet bitSet = new BitSet(size); int i = 1; BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { - mBitSet.set(++i % mSize); + bitSet.set(++i % size); } } @Test - public void timeSetOn() { + @Parameters(method = "getData") + public void timeSetOn(int size) { + BitSet bitSet = new BitSet(size); int i = 1; BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { - mBitSet.set(++i % mSize, true); + bitSet.set(++i % size, true); } } @Test - public void timeSetOff() { + @Parameters(method = "getData") + public void timeSetOff(int size) { + BitSet bitSet = new BitSet(size); int i = 1; BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { - mBitSet.set(++i % mSize, false); + bitSet.set(++i % size, false); } } } diff --git a/apct-tests/perftests/core/src/android/libcore/regression/BreakIteratorPerfTest.java b/apct-tests/perftests/core/src/android/libcore/regression/BreakIteratorPerfTest.java index 3952c12b3bfe..6a2ce5847daa 100644 --- a/apct-tests/perftests/core/src/android/libcore/regression/BreakIteratorPerfTest.java +++ b/apct-tests/perftests/core/src/android/libcore/regression/BreakIteratorPerfTest.java @@ -20,18 +20,19 @@ import android.perftests.utils.BenchmarkState; import android.perftests.utils.PerfStatusReporter; import android.test.suitebuilder.annotation.LargeTest; +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; + import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameters; import java.text.BreakIterator; import java.util.Arrays; import java.util.Collection; import java.util.Locale; -@RunWith(Parameterized.class) +@RunWith(JUnitParamsRunner.class) @LargeTest public final class BreakIteratorPerfTest { @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); @@ -41,36 +42,37 @@ public final class BreakIteratorPerfTest { Locale.US, "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi mollis consequat" + " nisl non pharetra. Praesent pretium vehicula odio sed ultrices. Aenean a" - + " felis libero. Vivamus sed commodo nibh. Pellentesque turpis lectus, euismod" - + " vel ante nec, cursus posuere orci. Suspendisse velit neque, fermentum" - + " luctus ultrices in, ultrices vitae arcu. Duis tincidunt cursus lorem. Nam" - + " ultricies accumsan quam vitae imperdiet. Pellentesque habitant morbi" - + " tristique senectus et netus et malesuada fames ac turpis egestas. Quisque" - + " aliquet pretium nisi, eget laoreet enim molestie sit amet. Class aptent" - + " taciti sociosqu ad litora torquent per conubia nostra, per inceptos" + + " felis libero. Vivamus sed commodo nibh. Pellentesque turpis lectus," + + " euismod vel ante nec, cursus posuere orci. Suspendisse velit neque," + + " fermentum luctus ultrices in, ultrices vitae arcu. Duis tincidunt cursus" + + " lorem. Nam ultricies accumsan quam vitae imperdiet. Pellentesque habitant" + + " morbi tristique senectus et netus et malesuada fames ac turpis egestas." + + " Quisque aliquet pretium nisi, eget laoreet enim molestie sit amet. Class" + + " aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos" + " himenaeos.\n" + "Nam dapibus aliquam lacus ac suscipit. Proin in nibh sit amet purus congue" + " laoreet eget quis nisl. Morbi gravida dignissim justo, a venenatis ante" - + " pulvinar at. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin" - + " ultrices vestibulum dui, vel aliquam lacus aliquam quis. Duis fringilla" - + " sapien ac lacus egestas, vel adipiscing elit euismod. Donec non tellus" - + " odio. Donec gravida eu massa ac feugiat. Aliquam erat volutpat. Praesent id" - + " adipiscing metus, nec laoreet enim. Aliquam vitae posuere turpis. Mauris ac" - + " pharetra sem. In at placerat tortor. Vivamus ac vehicula neque. Cras" - + " volutpat ullamcorper massa et varius. Praesent sagittis neque vitae nulla" - + " euismod pharetra.\n" + + " pulvinar at. Lorem ipsum dolor sit amet, consectetur adipiscing elit." + + " Proin ultrices vestibulum dui, vel aliquam lacus aliquam quis. Duis" + + " fringilla sapien ac lacus egestas, vel adipiscing elit euismod. Donec non" + + " tellus odio. Donec gravida eu massa ac feugiat. Aliquam erat volutpat." + + " Praesent id adipiscing metus, nec laoreet enim. Aliquam vitae posuere" + + " turpis. Mauris ac pharetra sem. In at placerat tortor. Vivamus ac vehicula" + + " neque. Cras volutpat ullamcorper massa et varius. Praesent sagittis neque" + + " vitae nulla euismod pharetra.\n" + "Sed placerat sapien non molestie sollicitudin. Nullam sit amet dictum quam." + " Etiam tincidunt tortor vel pretium vehicula. Praesent fringilla ipsum vel" + " velit luctus dignissim. Nulla massa ligula, mattis in enim et, mattis" + " lacinia odio. Suspendisse tristique urna a orci commodo tempor. Duis" + " lacinia egestas arcu a sollicitudin.\n" + "In ac feugiat lacus. Nunc fermentum eu est at tristique. Pellentesque quis" - + " ligula et orci placerat lacinia. Maecenas quis mauris diam. Etiam mi ipsum," - + " tempus in purus quis, euismod faucibus orci. Nulla facilisi. Praesent sit" - + " amet sapien vel elit porta adipiscing. Phasellus sit amet volutpat diam.\n" - + "Proin bibendum elit non lacus pharetra, quis eleifend tellus placerat. Nulla" - + " facilisi. Maecenas ante diam, pellentesque mattis mattis in, porta ut" - + " lorem. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices" + + " ligula et orci placerat lacinia. Maecenas quis mauris diam. Etiam mi" + + " ipsum, tempus in purus quis, euismod faucibus orci. Nulla facilisi." + + " Praesent sit amet sapien vel elit porta adipiscing. Phasellus sit amet" + + " volutpat diam.\n" + + "Proin bibendum elit non lacus pharetra, quis eleifend tellus placerat." + + " Nulla facilisi. Maecenas ante diam, pellentesque mattis mattis in, porta" + + " ut lorem. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices" + " posuere cubilia Curae; Nunc interdum tristique metus, in scelerisque odio" + " fermentum eget. Cras nec venenatis lacus. Aenean euismod eget metus quis" + " molestie. Cras tincidunt dolor ut massa ornare, in elementum lacus auctor." @@ -80,29 +82,29 @@ public final class BreakIteratorPerfTest { LONGPARA( Locale.US, "During dinner, Mr. Bennet scarcely spoke at all; but when the servants were" - + " withdrawn, he thought it time to have some conversation with his guest, and" - + " therefore started a subject in which he expected him to shine, by observing" - + " that he seemed very fortunate in his patroness. Lady Catherine de Bourgh's" - + " attention to his wishes, and consideration for his comfort, appeared very" - + " remarkable. Mr. Bennet could not have chosen better. Mr. Collins was" - + " eloquent in her praise. The subject elevated him to more than usual" - + " solemnity of manner, and with a most important aspect he protested that" - + " \"he had never in his life witnessed such behaviour in a person of" - + " rank--such affability and condescension, as he had himself experienced from" - + " Lady Catherine. She had been graciously pleased to approve of both of the" - + " discourses which he had already had the honour of preaching before her. She" - + " had also asked him twice to dine at Rosings, and had sent for him only the" - + " Saturday before, to make up her pool of quadrille in the evening. Lady" - + " Catherine was reckoned proud by many people he knew, but _he_ had never" - + " seen anything but affability in her. She had always spoken to him as she" - + " would to any other gentleman; she made not the smallest objection to his" - + " joining in the society of the neighbourhood nor to his leaving the parish" - + " occasionally for a week or two, to visit his relations. She had even" - + " condescended to advise him to marry as soon as he could, provided he chose" - + " with discretion; and had once paid him a visit in his humble parsonage," - + " where she had perfectly approved all the alterations he had been making," - + " and had even vouchsafed to suggest some herself--some shelves in the closet" - + " up stairs.\""), + + " withdrawn, he thought it time to have some conversation with his guest," + + " and therefore started a subject in which he expected him to shine, by" + + " observing that he seemed very fortunate in his patroness. Lady Catherine" + + " de Bourgh's attention to his wishes, and consideration for his comfort," + + " appeared very remarkable. Mr. Bennet could not have chosen better. Mr." + + " Collins was eloquent in her praise. The subject elevated him to more than" + + " usual solemnity of manner, and with a most important aspect he protested" + + " that \"he had never in his life witnessed such behaviour in a person of" + + " rank--such affability and condescension, as he had himself experienced" + + " from Lady Catherine. She had been graciously pleased to approve of both of" + + " the discourses which he had already had the honour of preaching before" + + " her. She had also asked him twice to dine at Rosings, and had sent for him" + + " only the Saturday before, to make up her pool of quadrille in the evening." + + " Lady Catherine was reckoned proud by many people he knew, but _he_ had" + + " never seen anything but affability in her. She had always spoken to him as" + + " she would to any other gentleman; she made not the smallest objection to" + + " his joining in the society of the neighbourhood nor to his leaving the" + + " parish occasionally for a week or two, to visit his relations. She had" + + " even condescended to advise him to marry as soon as he could, provided he" + + " chose with discretion; and had once paid him a visit in his humble" + + " parsonage, where she had perfectly approved all the alterations he had" + + " been making, and had even vouchsafed to suggest some herself--some shelves" + + " in the closet up stairs.\""), GERMAN( Locale.GERMANY, "Aber dieser Freiheit setzte endlich der Winter ein Ziel. Draußen auf den Feldern" @@ -119,15 +121,14 @@ public final class BreakIteratorPerfTest { + " เดิมทีเป็นการผสมผสานกันระหว่างสำเนียงอยุธยาและชาวไทยเชื้อสายจีนรุ่นหลังที่" + "พูดไทยแทนกลุ่มภาษาจีน" + " ลักษณะเด่นคือมีการออกเสียงที่ชัดเจนและแข็งกระด้างซึ่งได้รับอิทธิพลจากภาษาแต" - + "้จิ๋ว" - + " การออกเสียงพยัญชนะ สระ การผันวรรณยุกต์ที่ในภาษาไทยมาตรฐาน" + + "้จิ๋ว การออกเสียงพยัญชนะ สระ การผันวรรณยุกต์ที่ในภาษาไทยมาตรฐาน" + " มาจากสำเนียงถิ่นนี้ในขณะที่ภาษาไทยสำเนียงอื่นล้วนเหน่อทั้งสิ้น" + " คำศัพท์ที่ใช้ในสำเนียงกรุงเทพจำนวนมากได้รับมาจากกลุ่มภาษาจีนเช่นคำว่า โป๊," + " เฮ็ง, อาหมวย, อาซิ่ม ซึ่งมาจากภาษาแต้จิ๋ว และจากภาษาจีนเช่น ถู(涂), ชิ่ว(去" + " อ่านว่า\"ชู่\") และคำว่า ทาย(猜 อ่านว่า \"ชาย\") เป็นต้น" + " เนื่องจากสำเนียงกรุงเทพได้รับอิทธิพลมาจากภาษาจีนดังนั้นตัวอักษร \"ร\"" - + " มักออกเสียงเหมารวมเป็น \"ล\" หรือคำควบกล่ำบางคำถูกละทิ้งไปด้วยเช่น รู้ เป็น" - + " ลู้, เรื่อง เป็น เลื่อง หรือ ประเทศ เป็น ปะเทศ" + + " มักออกเสียงเหมารวมเป็น \"ล\" หรือคำควบกล่ำบางคำถูกละทิ้งไปด้วยเช่น รู้" + + " เป็น ลู้, เรื่อง เป็น เลื่อง หรือ ประเทศ เป็น ปะเทศ" + " เป็นต้นสร้างความลำบากให้แก่ต่างชาติที่ต้องการเรียนภาษาไทย" + " แต่อย่างไรก็ตามผู้ที่พูดสำเนียงถิ่นนี้ก็สามารถออกอักขระภาษาไทยตามมาตรฐานได" + "้อย่างถูกต้องเพียงแต่มักเผลอไม่ค่อยออกเสียง"), @@ -151,8 +152,7 @@ public final class BreakIteratorPerfTest { } } - @Parameters(name = "mText={0}") - public static Collection<Object[]> data() { + public static Collection<Object[]> getData() { return Arrays.asList( new Object[][] { {Text.ACCENT}, {Text.BIDI}, {Text.EMOJI}, {Text.EMPTY}, {Text.GERMAN}, @@ -161,15 +161,13 @@ public final class BreakIteratorPerfTest { }); } - @Parameterized.Parameter(0) - public Text mText; - @Test - public void timeBreakIterator() { + @Parameters(method = "getData") + public void timeBreakIterator(Text text) { BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { - BreakIterator it = BreakIterator.getLineInstance(mText.mLocale); - it.setText(mText.mText); + BreakIterator it = BreakIterator.getLineInstance(text.mLocale); + it.setText(text.mText); while (it.next() != BreakIterator.DONE) { // Keep iterating @@ -178,12 +176,13 @@ public final class BreakIteratorPerfTest { } @Test - public void timeIcuBreakIterator() { + @Parameters(method = "getData") + public void timeIcuBreakIterator(Text text) { BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { android.icu.text.BreakIterator it = - android.icu.text.BreakIterator.getLineInstance(mText.mLocale); - it.setText(mText.mText); + android.icu.text.BreakIterator.getLineInstance(text.mLocale); + it.setText(text.mText); while (it.next() != android.icu.text.BreakIterator.DONE) { // Keep iterating diff --git a/apct-tests/perftests/core/src/android/libcore/regression/BulkPerfTest.java b/apct-tests/perftests/core/src/android/libcore/regression/BulkPerfTest.java index 855bb9a43d34..b7b7e83f147c 100644 --- a/apct-tests/perftests/core/src/android/libcore/regression/BulkPerfTest.java +++ b/apct-tests/perftests/core/src/android/libcore/regression/BulkPerfTest.java @@ -20,11 +20,12 @@ import android.perftests.utils.BenchmarkState; import android.perftests.utils.PerfStatusReporter; import android.test.suitebuilder.annotation.LargeTest; +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; + import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameters; import java.io.File; import java.io.IOException; @@ -34,13 +35,12 @@ import java.nio.channels.FileChannel; import java.util.Arrays; import java.util.Collection; -@RunWith(Parameterized.class) +@RunWith(JUnitParamsRunner.class) @LargeTest public class BulkPerfTest { @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); - @Parameters(name = "mAlign({0}), mSBuf({1}), mDBuf({2}), mSize({3})") - public static Collection<Object[]> data() { + public static Collection<Object[]> getData() { return Arrays.asList( new Object[][] { {true, MyBufferType.DIRECT, MyBufferType.DIRECT, 4096}, @@ -82,24 +82,12 @@ public class BulkPerfTest { }); } - @Parameterized.Parameter(0) - public boolean mAlign; - enum MyBufferType { DIRECT, HEAP, MAPPED } - @Parameterized.Parameter(1) - public MyBufferType mSBuf; - - @Parameterized.Parameter(2) - public MyBufferType mDBuf; - - @Parameterized.Parameter(3) - public int mSize; - public static ByteBuffer newBuffer(boolean aligned, MyBufferType bufferType, int bsize) throws IOException { int size = aligned ? bsize : bsize + 8 + 1; @@ -126,13 +114,15 @@ public class BulkPerfTest { } @Test - public void timePut() throws Exception { - ByteBuffer src = BulkPerfTest.newBuffer(mAlign, mSBuf, mSize); - ByteBuffer data = BulkPerfTest.newBuffer(mAlign, mDBuf, mSize); + @Parameters(method = "getData") + public void timePut(boolean align, MyBufferType sBuf, MyBufferType dBuf, int size) + throws Exception { + ByteBuffer src = BulkPerfTest.newBuffer(align, sBuf, size); + ByteBuffer data = BulkPerfTest.newBuffer(align, dBuf, size); BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { - src.position(mAlign ? 0 : 1); - data.position(mAlign ? 0 : 1); + src.position(align ? 0 : 1); + data.position(align ? 0 : 1); src.put(data); } } diff --git a/apct-tests/perftests/core/src/android/libcore/regression/ByteBufferPerfTest.java b/apct-tests/perftests/core/src/android/libcore/regression/ByteBufferPerfTest.java index 4bd7c4e4fa82..9ac36d076c7b 100644 --- a/apct-tests/perftests/core/src/android/libcore/regression/ByteBufferPerfTest.java +++ b/apct-tests/perftests/core/src/android/libcore/regression/ByteBufferPerfTest.java @@ -20,11 +20,12 @@ import android.perftests.utils.BenchmarkState; import android.perftests.utils.PerfStatusReporter; import android.test.suitebuilder.annotation.LargeTest; +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; + import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameters; import java.io.File; import java.io.IOException; @@ -41,7 +42,7 @@ import java.nio.channels.FileChannel; import java.util.Arrays; import java.util.Collection; -@RunWith(Parameterized.class) +@RunWith(JUnitParamsRunner.class) @LargeTest public class ByteBufferPerfTest { @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); @@ -49,15 +50,14 @@ public class ByteBufferPerfTest { public enum MyByteOrder { BIG(ByteOrder.BIG_ENDIAN), LITTLE(ByteOrder.LITTLE_ENDIAN); - final ByteOrder mByteOrder; + final ByteOrder byteOrder; - MyByteOrder(ByteOrder mByteOrder) { - this.mByteOrder = mByteOrder; + MyByteOrder(ByteOrder byteOrder) { + this.byteOrder = byteOrder; } } - @Parameters(name = "mByteOrder={0}, mAligned={1}, mBufferType={2}") - public static Collection<Object[]> data() { + public static Collection<Object[]> getData() { return Arrays.asList( new Object[][] { {MyByteOrder.BIG, true, MyBufferType.DIRECT}, @@ -75,21 +75,12 @@ public class ByteBufferPerfTest { }); } - @Parameterized.Parameter(0) - public MyByteOrder mByteOrder; - - @Parameterized.Parameter(1) - public boolean mAligned; - enum MyBufferType { DIRECT, HEAP, MAPPED; } - @Parameterized.Parameter(2) - public MyBufferType mBufferType; - public static ByteBuffer newBuffer( MyByteOrder byteOrder, boolean aligned, MyBufferType bufferType) throws IOException { int size = aligned ? 8192 : 8192 + 8 + 1; @@ -115,7 +106,7 @@ public class ByteBufferPerfTest { result = fc.map(FileChannel.MapMode.READ_WRITE, 0, fc.size()); break; } - result.order(byteOrder.mByteOrder); + result.order(byteOrder.byteOrder); result.position(aligned ? 0 : 1); return result; } @@ -125,11 +116,13 @@ public class ByteBufferPerfTest { // @Test - public void timeByteBuffer_getByte() throws Exception { - ByteBuffer src = ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType); + @Parameters(method = "getData") + public void timeByteBuffer_getByte( + MyByteOrder byteOrder, boolean aligned, MyBufferType bufferType) throws Exception { + ByteBuffer src = ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType); BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { - src.position(mAligned ? 0 : 1); + src.position(aligned ? 0 : 1); for (int i = 0; i < 1024; ++i) { src.get(); } @@ -137,24 +130,28 @@ public class ByteBufferPerfTest { } @Test - public void timeByteBuffer_getByteArray() throws Exception { - ByteBuffer src = ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType); + @Parameters(method = "getData") + public void timeByteBuffer_getByteArray( + MyByteOrder byteOrder, boolean aligned, MyBufferType bufferType) throws Exception { + ByteBuffer src = ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType); byte[] dst = new byte[1024]; BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { for (int i = 0; i < 1024; ++i) { - src.position(mAligned ? 0 : 1); + src.position(aligned ? 0 : 1); src.get(dst); } } } @Test - public void timeByteBuffer_getByte_indexed() throws Exception { - ByteBuffer src = ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType); + @Parameters(method = "getData") + public void timeByteBuffer_getByte_indexed( + MyByteOrder byteOrder, boolean aligned, MyBufferType bufferType) throws Exception { + ByteBuffer src = ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType); BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { - src.position(mAligned ? 0 : 1); + src.position(aligned ? 0 : 1); for (int i = 0; i < 1024; ++i) { src.get(i); } @@ -162,11 +159,13 @@ public class ByteBufferPerfTest { } @Test - public void timeByteBuffer_getChar() throws Exception { - ByteBuffer src = ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType); + @Parameters(method = "getData") + public void timeByteBuffer_getChar( + MyByteOrder byteOrder, boolean aligned, MyBufferType bufferType) throws Exception { + ByteBuffer src = ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType); BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { - src.position(mAligned ? 0 : 1); + src.position(aligned ? 0 : 1); for (int i = 0; i < 1024; ++i) { src.getChar(); } @@ -174,9 +173,11 @@ public class ByteBufferPerfTest { } @Test - public void timeCharBuffer_getCharArray() throws Exception { + @Parameters(method = "getData") + public void timeCharBuffer_getCharArray( + MyByteOrder byteOrder, boolean aligned, MyBufferType bufferType) throws Exception { CharBuffer src = - ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType).asCharBuffer(); + ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType).asCharBuffer(); char[] dst = new char[1024]; BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { @@ -188,11 +189,13 @@ public class ByteBufferPerfTest { } @Test - public void timeByteBuffer_getChar_indexed() throws Exception { - ByteBuffer src = ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType); + @Parameters(method = "getData") + public void timeByteBuffer_getChar_indexed( + MyByteOrder byteOrder, boolean aligned, MyBufferType bufferType) throws Exception { + ByteBuffer src = ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType); BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { - src.position(mAligned ? 0 : 1); + src.position(aligned ? 0 : 1); for (int i = 0; i < 1024; ++i) { src.getChar(i * 2); } @@ -200,11 +203,13 @@ public class ByteBufferPerfTest { } @Test - public void timeByteBuffer_getDouble() throws Exception { - ByteBuffer src = ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType); + @Parameters(method = "getData") + public void timeByteBuffer_getDouble( + MyByteOrder byteOrder, boolean aligned, MyBufferType bufferType) throws Exception { + ByteBuffer src = ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType); BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { - src.position(mAligned ? 0 : 1); + src.position(aligned ? 0 : 1); for (int i = 0; i < 1024; ++i) { src.getDouble(); } @@ -212,9 +217,11 @@ public class ByteBufferPerfTest { } @Test - public void timeDoubleBuffer_getDoubleArray() throws Exception { + @Parameters(method = "getData") + public void timeDoubleBuffer_getDoubleArray( + MyByteOrder byteOrder, boolean aligned, MyBufferType bufferType) throws Exception { DoubleBuffer src = - ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType).asDoubleBuffer(); + ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType).asDoubleBuffer(); double[] dst = new double[1024]; BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { @@ -226,11 +233,13 @@ public class ByteBufferPerfTest { } @Test - public void timeByteBuffer_getFloat() throws Exception { - ByteBuffer src = ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType); + @Parameters(method = "getData") + public void timeByteBuffer_getFloat( + MyByteOrder byteOrder, boolean aligned, MyBufferType bufferType) throws Exception { + ByteBuffer src = ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType); BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { - src.position(mAligned ? 0 : 1); + src.position(aligned ? 0 : 1); for (int i = 0; i < 1024; ++i) { src.getFloat(); } @@ -238,9 +247,11 @@ public class ByteBufferPerfTest { } @Test - public void timeFloatBuffer_getFloatArray() throws Exception { + @Parameters(method = "getData") + public void timeFloatBuffer_getFloatArray( + MyByteOrder byteOrder, boolean aligned, MyBufferType bufferType) throws Exception { FloatBuffer src = - ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType).asFloatBuffer(); + ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType).asFloatBuffer(); float[] dst = new float[1024]; BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { @@ -252,11 +263,13 @@ public class ByteBufferPerfTest { } @Test - public void timeByteBuffer_getInt() throws Exception { - ByteBuffer src = ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType); + @Parameters(method = "getData") + public void timeByteBuffer_getInt( + MyByteOrder byteOrder, boolean aligned, MyBufferType bufferType) throws Exception { + ByteBuffer src = ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType); BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { - src.position(mAligned ? 0 : 1); + src.position(aligned ? 0 : 1); for (int i = 0; i < 1024; ++i) { src.getInt(); } @@ -264,9 +277,10 @@ public class ByteBufferPerfTest { } @Test - public void timeIntBuffer_getIntArray() throws Exception { - IntBuffer src = - ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType).asIntBuffer(); + @Parameters(method = "getData") + public void timeIntBuffer_getIntArray( + MyByteOrder byteOrder, boolean aligned, MyBufferType bufferType) throws Exception { + IntBuffer src = ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType).asIntBuffer(); int[] dst = new int[1024]; BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { @@ -278,11 +292,13 @@ public class ByteBufferPerfTest { } @Test - public void timeByteBuffer_getLong() throws Exception { - ByteBuffer src = ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType); + @Parameters(method = "getData") + public void timeByteBuffer_getLong( + MyByteOrder byteOrder, boolean aligned, MyBufferType bufferType) throws Exception { + ByteBuffer src = ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType); BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { - src.position(mAligned ? 0 : 1); + src.position(aligned ? 0 : 1); for (int i = 0; i < 1024; ++i) { src.getLong(); } @@ -290,9 +306,11 @@ public class ByteBufferPerfTest { } @Test - public void timeLongBuffer_getLongArray() throws Exception { + @Parameters(method = "getData") + public void timeLongBuffer_getLongArray( + MyByteOrder byteOrder, boolean aligned, MyBufferType bufferType) throws Exception { LongBuffer src = - ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType).asLongBuffer(); + ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType).asLongBuffer(); long[] dst = new long[1024]; BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { @@ -304,11 +322,13 @@ public class ByteBufferPerfTest { } @Test - public void timeByteBuffer_getShort() throws Exception { - ByteBuffer src = ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType); + @Parameters(method = "getData") + public void timeByteBuffer_getShort( + MyByteOrder byteOrder, boolean aligned, MyBufferType bufferType) throws Exception { + ByteBuffer src = ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType); BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { - src.position(mAligned ? 0 : 1); + src.position(aligned ? 0 : 1); for (int i = 0; i < 1024; ++i) { src.getShort(); } @@ -316,9 +336,11 @@ public class ByteBufferPerfTest { } @Test - public void timeShortBuffer_getShortArray() throws Exception { + @Parameters(method = "getData") + public void timeShortBuffer_getShortArray( + MyByteOrder byteOrder, boolean aligned, MyBufferType bufferType) throws Exception { ShortBuffer src = - ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType).asShortBuffer(); + ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType).asShortBuffer(); short[] dst = new short[1024]; BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { @@ -334,8 +356,10 @@ public class ByteBufferPerfTest { // @Test - public void timeByteBuffer_putByte() throws Exception { - ByteBuffer src = ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType); + @Parameters(method = "getData") + public void timeByteBuffer_putByte( + MyByteOrder byteOrder, boolean aligned, MyBufferType bufferType) throws Exception { + ByteBuffer src = ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType); BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { src.position(0); @@ -346,24 +370,28 @@ public class ByteBufferPerfTest { } @Test - public void timeByteBuffer_putByteArray() throws Exception { - ByteBuffer dst = ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType); + @Parameters(method = "getData") + public void timeByteBuffer_putByteArray( + MyByteOrder byteOrder, boolean aligned, MyBufferType bufferType) throws Exception { + ByteBuffer dst = ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType); byte[] src = new byte[1024]; BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { for (int i = 0; i < 1024; ++i) { - dst.position(mAligned ? 0 : 1); + dst.position(aligned ? 0 : 1); dst.put(src); } } } @Test - public void timeByteBuffer_putChar() throws Exception { - ByteBuffer src = ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType); + @Parameters(method = "getData") + public void timeByteBuffer_putChar( + MyByteOrder byteOrder, boolean aligned, MyBufferType bufferType) throws Exception { + ByteBuffer src = ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType); BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { - src.position(mAligned ? 0 : 1); + src.position(aligned ? 0 : 1); for (int i = 0; i < 1024; ++i) { src.putChar(' '); } @@ -371,9 +399,11 @@ public class ByteBufferPerfTest { } @Test - public void timeCharBuffer_putCharArray() throws Exception { + @Parameters(method = "getData") + public void timeCharBuffer_putCharArray( + MyByteOrder byteOrder, boolean aligned, MyBufferType bufferType) throws Exception { CharBuffer dst = - ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType).asCharBuffer(); + ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType).asCharBuffer(); char[] src = new char[1024]; BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { @@ -385,11 +415,13 @@ public class ByteBufferPerfTest { } @Test - public void timeByteBuffer_putDouble() throws Exception { - ByteBuffer src = ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType); + @Parameters(method = "getData") + public void timeByteBuffer_putDouble( + MyByteOrder byteOrder, boolean aligned, MyBufferType bufferType) throws Exception { + ByteBuffer src = ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType); BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { - src.position(mAligned ? 0 : 1); + src.position(aligned ? 0 : 1); for (int i = 0; i < 1024; ++i) { src.putDouble(0.0); } @@ -397,9 +429,11 @@ public class ByteBufferPerfTest { } @Test - public void timeDoubleBuffer_putDoubleArray() throws Exception { + @Parameters(method = "getData") + public void timeDoubleBuffer_putDoubleArray( + MyByteOrder byteOrder, boolean aligned, MyBufferType bufferType) throws Exception { DoubleBuffer dst = - ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType).asDoubleBuffer(); + ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType).asDoubleBuffer(); double[] src = new double[1024]; BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { @@ -411,11 +445,13 @@ public class ByteBufferPerfTest { } @Test - public void timeByteBuffer_putFloat() throws Exception { - ByteBuffer src = ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType); + @Parameters(method = "getData") + public void timeByteBuffer_putFloat( + MyByteOrder byteOrder, boolean aligned, MyBufferType bufferType) throws Exception { + ByteBuffer src = ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType); BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { - src.position(mAligned ? 0 : 1); + src.position(aligned ? 0 : 1); for (int i = 0; i < 1024; ++i) { src.putFloat(0.0f); } @@ -423,9 +459,11 @@ public class ByteBufferPerfTest { } @Test - public void timeFloatBuffer_putFloatArray() throws Exception { + @Parameters(method = "getData") + public void timeFloatBuffer_putFloatArray( + MyByteOrder byteOrder, boolean aligned, MyBufferType bufferType) throws Exception { FloatBuffer dst = - ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType).asFloatBuffer(); + ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType).asFloatBuffer(); float[] src = new float[1024]; BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { @@ -437,11 +475,13 @@ public class ByteBufferPerfTest { } @Test - public void timeByteBuffer_putInt() throws Exception { - ByteBuffer src = ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType); + @Parameters(method = "getData") + public void timeByteBuffer_putInt( + MyByteOrder byteOrder, boolean aligned, MyBufferType bufferType) throws Exception { + ByteBuffer src = ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType); BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { - src.position(mAligned ? 0 : 1); + src.position(aligned ? 0 : 1); for (int i = 0; i < 1024; ++i) { src.putInt(0); } @@ -449,9 +489,10 @@ public class ByteBufferPerfTest { } @Test - public void timeIntBuffer_putIntArray() throws Exception { - IntBuffer dst = - ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType).asIntBuffer(); + @Parameters(method = "getData") + public void timeIntBuffer_putIntArray( + MyByteOrder byteOrder, boolean aligned, MyBufferType bufferType) throws Exception { + IntBuffer dst = ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType).asIntBuffer(); int[] src = new int[1024]; BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { @@ -463,11 +504,13 @@ public class ByteBufferPerfTest { } @Test - public void timeByteBuffer_putLong() throws Exception { - ByteBuffer src = ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType); + @Parameters(method = "getData") + public void timeByteBuffer_putLong( + MyByteOrder byteOrder, boolean aligned, MyBufferType bufferType) throws Exception { + ByteBuffer src = ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType); BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { - src.position(mAligned ? 0 : 1); + src.position(aligned ? 0 : 1); for (int i = 0; i < 1024; ++i) { src.putLong(0L); } @@ -475,9 +518,11 @@ public class ByteBufferPerfTest { } @Test - public void timeLongBuffer_putLongArray() throws Exception { + @Parameters(method = "getData") + public void timeLongBuffer_putLongArray( + MyByteOrder byteOrder, boolean aligned, MyBufferType bufferType) throws Exception { LongBuffer dst = - ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType).asLongBuffer(); + ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType).asLongBuffer(); long[] src = new long[1024]; BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { @@ -489,11 +534,13 @@ public class ByteBufferPerfTest { } @Test - public void timeByteBuffer_putShort() throws Exception { - ByteBuffer src = ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType); + @Parameters(method = "getData") + public void timeByteBuffer_putShort( + MyByteOrder byteOrder, boolean aligned, MyBufferType bufferType) throws Exception { + ByteBuffer src = ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType); BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { - src.position(mAligned ? 0 : 1); + src.position(aligned ? 0 : 1); for (int i = 0; i < 1024; ++i) { src.putShort((short) 0); } @@ -501,9 +548,11 @@ public class ByteBufferPerfTest { } @Test - public void timeShortBuffer_putShortArray() throws Exception { + @Parameters(method = "getData") + public void timeShortBuffer_putShortArray( + MyByteOrder byteOrder, boolean aligned, MyBufferType bufferType) throws Exception { ShortBuffer dst = - ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType).asShortBuffer(); + ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType).asShortBuffer(); short[] src = new short[1024]; BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { @@ -515,6 +564,7 @@ public class ByteBufferPerfTest { } @Test + @Parameters(method = "getData") public void time_new_byteArray() throws Exception { BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { @@ -523,6 +573,7 @@ public class ByteBufferPerfTest { } @Test + @Parameters(method = "getData") public void time_ByteBuffer_allocate() throws Exception { BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { diff --git a/apct-tests/perftests/core/src/android/libcore/regression/ByteBufferScalarVersusVectorPerfTest.java b/apct-tests/perftests/core/src/android/libcore/regression/ByteBufferScalarVersusVectorPerfTest.java index 81f9e59f2423..5dd9d6e2bc2c 100644 --- a/apct-tests/perftests/core/src/android/libcore/regression/ByteBufferScalarVersusVectorPerfTest.java +++ b/apct-tests/perftests/core/src/android/libcore/regression/ByteBufferScalarVersusVectorPerfTest.java @@ -20,23 +20,23 @@ import android.perftests.utils.BenchmarkState; import android.perftests.utils.PerfStatusReporter; import android.test.suitebuilder.annotation.LargeTest; +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; + import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameters; import java.nio.ByteBuffer; import java.util.Arrays; import java.util.Collection; -@RunWith(Parameterized.class) +@RunWith(JUnitParamsRunner.class) @LargeTest public class ByteBufferScalarVersusVectorPerfTest { @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); - @Parameters(name = "mByteOrder={0}, mAligned={1}, mBufferType={2}") - public static Collection<Object[]> data() { + public static Collection<Object[]> getData() { return Arrays.asList( new Object[][] { { @@ -102,19 +102,15 @@ public class ByteBufferScalarVersusVectorPerfTest { }); } - @Parameterized.Parameter(0) - public ByteBufferPerfTest.MyByteOrder mByteOrder; - - @Parameterized.Parameter(1) - public boolean mAligned; - - @Parameterized.Parameter(2) - public ByteBufferPerfTest.MyBufferType mBufferType; - @Test - public void timeManualByteBufferCopy() throws Exception { - ByteBuffer src = ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType); - ByteBuffer dst = ByteBufferPerfTest.newBuffer(mByteOrder, mAligned, mBufferType); + @Parameters(method = "getData") + public void timeManualByteBufferCopy( + ByteBufferPerfTest.MyByteOrder byteOrder, + boolean aligned, + ByteBufferPerfTest.MyBufferType bufferType) + throws Exception { + ByteBuffer src = ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType); + ByteBuffer dst = ByteBufferPerfTest.newBuffer(byteOrder, aligned, bufferType); BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { src.position(0); @@ -126,23 +122,25 @@ public class ByteBufferScalarVersusVectorPerfTest { } @Test - public void timeByteBufferBulkGet() throws Exception { - ByteBuffer src = ByteBuffer.allocate(mAligned ? 8192 : 8192 + 1); + @Parameters({"true", "false"}) + public void timeByteBufferBulkGet(boolean aligned) throws Exception { + ByteBuffer src = ByteBuffer.allocate(aligned ? 8192 : 8192 + 1); byte[] dst = new byte[8192]; BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { - src.position(mAligned ? 0 : 1); + src.position(aligned ? 0 : 1); src.get(dst, 0, dst.length); } } @Test - public void timeDirectByteBufferBulkGet() throws Exception { - ByteBuffer src = ByteBuffer.allocateDirect(mAligned ? 8192 : 8192 + 1); + @Parameters({"true", "false"}) + public void timeDirectByteBufferBulkGet(boolean aligned) throws Exception { + ByteBuffer src = ByteBuffer.allocateDirect(aligned ? 8192 : 8192 + 1); byte[] dst = new byte[8192]; BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { - src.position(mAligned ? 0 : 1); + src.position(aligned ? 0 : 1); src.get(dst, 0, dst.length); } } diff --git a/apct-tests/perftests/core/src/android/libcore/regression/CharacterPerfTest.java b/apct-tests/perftests/core/src/android/libcore/regression/CharacterPerfTest.java index 28ec6ded3c86..0a598998bced 100644 --- a/apct-tests/perftests/core/src/android/libcore/regression/CharacterPerfTest.java +++ b/apct-tests/perftests/core/src/android/libcore/regression/CharacterPerfTest.java @@ -20,12 +20,12 @@ import android.perftests.utils.BenchmarkState; import android.perftests.utils.PerfStatusReporter; import android.test.suitebuilder.annotation.LargeTest; -import org.junit.Before; +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; + import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameters; import java.util.Arrays; import java.util.Collection; @@ -34,13 +34,12 @@ import java.util.Collection; * Tests various Character methods, intended for testing multiple implementations against each * other. */ -@RunWith(Parameterized.class) +@RunWith(JUnitParamsRunner.class) @LargeTest public class CharacterPerfTest { @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); - @Parameters(name = "mCharacterSet({0}), mOverload({1})") - public static Collection<Object[]> data() { + public static Collection<Object[]> getData() { return Arrays.asList( new Object[][] { {CharacterSet.ASCII, Overload.CHAR}, @@ -50,17 +49,10 @@ public class CharacterPerfTest { }); } - @Parameterized.Parameter(0) - public CharacterSet mCharacterSet; - - @Parameterized.Parameter(1) - public Overload mOverload; - private char[] mChars; - @Before - public void setUp() throws Exception { - this.mChars = mCharacterSet.mChars; + public void setUp(CharacterSet characterSet) { + this.mChars = characterSet.mChars; } public enum Overload { @@ -87,10 +79,12 @@ public class CharacterPerfTest { // A fake benchmark to give us a baseline. @Test - public void timeIsSpace() { + @Parameters(method = "getData") + public void timeIsSpace(CharacterSet characterSet, Overload overload) { + setUp(characterSet); boolean fake = false; BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); - if (mOverload == Overload.CHAR) { + if (overload == Overload.CHAR) { while (state.keepRunning()) { for (int ch = 0; ch < 65536; ++ch) { fake ^= ((char) ch == ' '); @@ -106,9 +100,11 @@ public class CharacterPerfTest { } @Test - public void timeDigit() { + @Parameters(method = "getData") + public void timeDigit(CharacterSet characterSet, Overload overload) { + setUp(characterSet); BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); - if (mOverload == Overload.CHAR) { + if (overload == Overload.CHAR) { while (state.keepRunning()) { for (int ch = 0; ch < 65536; ++ch) { Character.digit(mChars[ch], 10); @@ -124,9 +120,11 @@ public class CharacterPerfTest { } @Test - public void timeGetNumericValue() { + @Parameters(method = "getData") + public void timeGetNumericValue(CharacterSet characterSet, Overload overload) { + setUp(characterSet); BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); - if (mOverload == Overload.CHAR) { + if (overload == Overload.CHAR) { while (state.keepRunning()) { for (int ch = 0; ch < 65536; ++ch) { Character.getNumericValue(mChars[ch]); @@ -142,9 +140,11 @@ public class CharacterPerfTest { } @Test - public void timeIsDigit() { + @Parameters(method = "getData") + public void timeIsDigit(CharacterSet characterSet, Overload overload) { + setUp(characterSet); BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); - if (mOverload == Overload.CHAR) { + if (overload == Overload.CHAR) { while (state.keepRunning()) { for (int ch = 0; ch < 65536; ++ch) { Character.isDigit(mChars[ch]); @@ -160,9 +160,11 @@ public class CharacterPerfTest { } @Test - public void timeIsIdentifierIgnorable() { + @Parameters(method = "getData") + public void timeIsIdentifierIgnorable(CharacterSet characterSet, Overload overload) { + setUp(characterSet); BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); - if (mOverload == Overload.CHAR) { + if (overload == Overload.CHAR) { while (state.keepRunning()) { for (int ch = 0; ch < 65536; ++ch) { Character.isIdentifierIgnorable(mChars[ch]); @@ -178,9 +180,11 @@ public class CharacterPerfTest { } @Test - public void timeIsJavaIdentifierPart() { + @Parameters(method = "getData") + public void timeIsJavaIdentifierPart(CharacterSet characterSet, Overload overload) { + setUp(characterSet); BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); - if (mOverload == Overload.CHAR) { + if (overload == Overload.CHAR) { while (state.keepRunning()) { for (int ch = 0; ch < 65536; ++ch) { Character.isJavaIdentifierPart(mChars[ch]); @@ -196,9 +200,11 @@ public class CharacterPerfTest { } @Test - public void timeIsJavaIdentifierStart() { + @Parameters(method = "getData") + public void timeIsJavaIdentifierStart(CharacterSet characterSet, Overload overload) { + setUp(characterSet); BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); - if (mOverload == Overload.CHAR) { + if (overload == Overload.CHAR) { while (state.keepRunning()) { for (int ch = 0; ch < 65536; ++ch) { Character.isJavaIdentifierStart(mChars[ch]); @@ -214,9 +220,11 @@ public class CharacterPerfTest { } @Test - public void timeIsLetter() { + @Parameters(method = "getData") + public void timeIsLetter(CharacterSet characterSet, Overload overload) { + setUp(characterSet); BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); - if (mOverload == Overload.CHAR) { + if (overload == Overload.CHAR) { while (state.keepRunning()) { for (int ch = 0; ch < 65536; ++ch) { Character.isLetter(mChars[ch]); @@ -232,9 +240,11 @@ public class CharacterPerfTest { } @Test - public void timeIsLetterOrDigit() { + @Parameters(method = "getData") + public void timeIsLetterOrDigit(CharacterSet characterSet, Overload overload) { + setUp(characterSet); BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); - if (mOverload == Overload.CHAR) { + if (overload == Overload.CHAR) { while (state.keepRunning()) { for (int ch = 0; ch < 65536; ++ch) { Character.isLetterOrDigit(mChars[ch]); @@ -250,9 +260,11 @@ public class CharacterPerfTest { } @Test - public void timeIsLowerCase() { + @Parameters(method = "getData") + public void timeIsLowerCase(CharacterSet characterSet, Overload overload) { + setUp(characterSet); BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); - if (mOverload == Overload.CHAR) { + if (overload == Overload.CHAR) { while (state.keepRunning()) { for (int ch = 0; ch < 65536; ++ch) { Character.isLowerCase(mChars[ch]); @@ -268,9 +280,11 @@ public class CharacterPerfTest { } @Test - public void timeIsSpaceChar() { + @Parameters(method = "getData") + public void timeIsSpaceChar(CharacterSet characterSet, Overload overload) { + setUp(characterSet); BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); - if (mOverload == Overload.CHAR) { + if (overload == Overload.CHAR) { while (state.keepRunning()) { for (int ch = 0; ch < 65536; ++ch) { Character.isSpaceChar(mChars[ch]); @@ -286,9 +300,11 @@ public class CharacterPerfTest { } @Test - public void timeIsUpperCase() { + @Parameters(method = "getData") + public void timeIsUpperCase(CharacterSet characterSet, Overload overload) { + setUp(characterSet); BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); - if (mOverload == Overload.CHAR) { + if (overload == Overload.CHAR) { while (state.keepRunning()) { for (int ch = 0; ch < 65536; ++ch) { Character.isUpperCase(mChars[ch]); @@ -304,9 +320,11 @@ public class CharacterPerfTest { } @Test - public void timeIsWhitespace() { + @Parameters(method = "getData") + public void timeIsWhitespace(CharacterSet characterSet, Overload overload) { + setUp(characterSet); BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); - if (mOverload == Overload.CHAR) { + if (overload == Overload.CHAR) { while (state.keepRunning()) { for (int ch = 0; ch < 65536; ++ch) { Character.isWhitespace(mChars[ch]); @@ -322,9 +340,11 @@ public class CharacterPerfTest { } @Test - public void timeToLowerCase() { + @Parameters(method = "getData") + public void timeToLowerCase(CharacterSet characterSet, Overload overload) { + setUp(characterSet); BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); - if (mOverload == Overload.CHAR) { + if (overload == Overload.CHAR) { while (state.keepRunning()) { for (int ch = 0; ch < 65536; ++ch) { Character.toLowerCase(mChars[ch]); @@ -340,9 +360,11 @@ public class CharacterPerfTest { } @Test - public void timeToUpperCase() { + @Parameters(method = "getData") + public void timeToUpperCase(CharacterSet characterSet, Overload overload) { + setUp(characterSet); BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); - if (mOverload == Overload.CHAR) { + if (overload == Overload.CHAR) { while (state.keepRunning()) { for (int ch = 0; ch < 65536; ++ch) { Character.toUpperCase(mChars[ch]); diff --git a/apct-tests/perftests/core/src/android/libcore/regression/CharsetForNamePerfTest.java b/apct-tests/perftests/core/src/android/libcore/regression/CharsetForNamePerfTest.java index 603b182e7c36..8da13a9b7f91 100644 --- a/apct-tests/perftests/core/src/android/libcore/regression/CharsetForNamePerfTest.java +++ b/apct-tests/perftests/core/src/android/libcore/regression/CharsetForNamePerfTest.java @@ -20,44 +20,40 @@ import android.perftests.utils.BenchmarkState; import android.perftests.utils.PerfStatusReporter; import android.test.suitebuilder.annotation.LargeTest; +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; + import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; import java.nio.charset.Charset; -import java.util.Arrays; -import java.util.Collection; -@RunWith(Parameterized.class) +@RunWith(JUnitParamsRunner.class) @LargeTest public class CharsetForNamePerfTest { @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); - @Parameterized.Parameters(name = "mCharsetName({0})") - public static Collection<Object[]> data() { - return Arrays.asList( - new Object[][] { - {"UTF-16"}, - {"UTF-8"}, - {"UTF8"}, - {"ISO-8859-1"}, - {"8859_1"}, - {"ISO-8859-2"}, - {"8859_2"}, - {"US-ASCII"}, - {"ASCII"}, - }); + public static String[] charsetNames() { + return new String[] { + "UTF-16", + "UTF-8", + "UTF8", + "ISO-8859-1", + "8859_1", + "ISO-8859-2", + "8859_2", + "US-ASCII", + "ASCII", + }; } - @Parameterized.Parameter(0) - public String mCharsetName; - @Test - public void timeCharsetForName() throws Exception { + @Parameters(method = "charsetNames") + public void timeCharsetForName(String charsetName) throws Exception { BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { - Charset.forName(mCharsetName); + Charset.forName(charsetName); } } } diff --git a/apct-tests/perftests/core/src/android/libcore/regression/CharsetPerfTest.java b/apct-tests/perftests/core/src/android/libcore/regression/CharsetPerfTest.java index 437d186834e0..048c50f044c8 100644 --- a/apct-tests/perftests/core/src/android/libcore/regression/CharsetPerfTest.java +++ b/apct-tests/perftests/core/src/android/libcore/regression/CharsetPerfTest.java @@ -20,22 +20,22 @@ import android.perftests.utils.BenchmarkState; import android.perftests.utils.PerfStatusReporter; import android.test.suitebuilder.annotation.LargeTest; +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; + import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameters; import java.util.Arrays; import java.util.Collection; -@RunWith(Parameterized.class) +@RunWith(JUnitParamsRunner.class) @LargeTest public class CharsetPerfTest { @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); - @Parameters(name = "mLength({0}), mName({1})") - public static Collection<Object[]> data() { + public static Collection<Object[]> getData() { return Arrays.asList( new Object[][] { {1, "UTF-16"}, @@ -86,24 +86,20 @@ public class CharsetPerfTest { }); } - @Parameterized.Parameter(0) - public int mLength; - - @Parameterized.Parameter(1) - public String mName; - @Test - public void time_new_String_BString() throws Exception { - byte[] bytes = makeBytes(makeString(mLength)); + @Parameters(method = "getData") + public void time_new_String_BString(int length, String name) throws Exception { + byte[] bytes = makeBytes(makeString(length)); BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { - new String(bytes, mName); + new String(bytes, name); } } @Test - public void time_new_String_BII() throws Exception { - byte[] bytes = makeBytes(makeString(mLength)); + @Parameters(method = "getData") + public void time_new_String_BII(int length, String name) throws Exception { + byte[] bytes = makeBytes(makeString(length)); BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { new String(bytes, 0, bytes.length); @@ -111,20 +107,22 @@ public class CharsetPerfTest { } @Test - public void time_new_String_BIIString() throws Exception { - byte[] bytes = makeBytes(makeString(mLength)); + @Parameters(method = "getData") + public void time_new_String_BIIString(int length, String name) throws Exception { + byte[] bytes = makeBytes(makeString(length)); BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { - new String(bytes, 0, bytes.length, mName); + new String(bytes, 0, bytes.length, name); } } @Test - public void time_String_getBytes() throws Exception { - String string = makeString(mLength); + @Parameters(method = "getData") + public void time_String_getBytes(int length, String name) throws Exception { + String string = makeString(length); BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { - string.getBytes(mName); + string.getBytes(name); } } diff --git a/apct-tests/perftests/core/src/android/libcore/regression/CipherPerfTest.java b/apct-tests/perftests/core/src/android/libcore/regression/CipherPerfTest.java index 15c27f2366e1..42b058815bfe 100644 --- a/apct-tests/perftests/core/src/android/libcore/regression/CipherPerfTest.java +++ b/apct-tests/perftests/core/src/android/libcore/regression/CipherPerfTest.java @@ -20,11 +20,12 @@ import android.perftests.utils.BenchmarkState; import android.perftests.utils.PerfStatusReporter; import android.test.suitebuilder.annotation.LargeTest; -import org.junit.Before; +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; + import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; import java.security.spec.AlgorithmParameterSpec; import java.util.ArrayList; @@ -42,17 +43,13 @@ import javax.crypto.spec.IvParameterSpec; * Cipher benchmarks. Only runs on AES currently because of the combinatorial explosion of the test * as it stands. */ -@RunWith(Parameterized.class) +@RunWith(JUnitParamsRunner.class) @LargeTest public class CipherPerfTest { @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); - @Parameterized.Parameters( - name = - "mMode({0}), mPadding({1}), mKeySize({2}), mInputSize({3})," - + " mImplementation({4})") - public static Collection cases() { - int[] mKeySizes = new int[] {128, 192, 256}; + public static Collection getCases() { + int[] keySizes = new int[] {128, 192, 256}; int[] inputSizes = new int[] {16, 32, 64, 128, 1024, 8192}; final List<Object[]> params = new ArrayList<>(); for (Mode mode : Mode.values()) { @@ -71,11 +68,11 @@ public class CipherPerfTest { && implementation == Implementation.OpenSSL) { continue; } - for (int mKeySize : mKeySizes) { + for (int keySize : keySizes) { for (int inputSize : inputSizes) { params.add( new Object[] { - mode, padding, mKeySize, inputSize, implementation + mode, padding, keySize, inputSize, implementation }); } } @@ -107,9 +104,6 @@ public class CipherPerfTest { AES, }; - @Parameterized.Parameter(0) - public Mode mMode; - public enum Mode { CBC, CFB, @@ -118,23 +112,11 @@ public class CipherPerfTest { OFB, }; - @Parameterized.Parameter(1) - public Padding mPadding; - public enum Padding { NOPADDING, PKCS1PADDING, }; - @Parameterized.Parameter(2) - public int mKeySize; - - @Parameterized.Parameter(3) - public int mInputSize; - - @Parameterized.Parameter(4) - public Implementation mImplementation; - public enum Implementation { OpenSSL, BouncyCastle @@ -156,21 +138,20 @@ public class CipherPerfTest { private AlgorithmParameterSpec mSpec; - @Before - public void setUp() throws Exception { - mCipherAlgorithm = - mAlgorithm.toString() + "/" + mMode.toString() + "/" + mPadding.toString(); + public void setUp(Mode mode, Padding padding, int keySize, Implementation implementation) + throws Exception { + mCipherAlgorithm = mAlgorithm.toString() + "/" + mode.toString() + "/" + padding.toString(); String mKeyAlgorithm = mAlgorithm.toString(); - mKey = sKeySizes.get(mKeySize); + mKey = sKeySizes.get(keySize); if (mKey == null) { KeyGenerator generator = KeyGenerator.getInstance(mKeyAlgorithm); - generator.init(mKeySize); + generator.init(keySize); mKey = generator.generateKey(); - sKeySizes.put(mKeySize, mKey); + sKeySizes.put(keySize, mKey); } - switch (mImplementation) { + switch (implementation) { case OpenSSL: mProviderName = "AndroidOpenSSL"; break; @@ -178,10 +159,10 @@ public class CipherPerfTest { mProviderName = "BC"; break; default: - throw new RuntimeException(mImplementation.toString()); + throw new RuntimeException(implementation.toString()); } - if (mMode != Mode.ECB) { + if (mode != Mode.ECB) { mSpec = new IvParameterSpec(IV); } @@ -193,18 +174,26 @@ public class CipherPerfTest { } @Test - public void timeEncrypt() throws Exception { + @Parameters(method = "getCases") + public void timeEncrypt( + Mode mode, Padding padding, int keySize, int inputSize, Implementation implementation) + throws Exception { + setUp(mode, padding, keySize, implementation); BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { - mCipherEncrypt.doFinal(DATA, 0, mInputSize, mOutput); + mCipherEncrypt.doFinal(DATA, 0, inputSize, mOutput); } } @Test - public void timeDecrypt() throws Exception { + @Parameters(method = "getCases") + public void timeDecrypt( + Mode mode, Padding padding, int keySize, int inputSize, Implementation implementation) + throws Exception { + setUp(mode, padding, keySize, implementation); BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { - mCipherDecrypt.doFinal(DATA, 0, mInputSize, mOutput); + mCipherDecrypt.doFinal(DATA, 0, inputSize, mOutput); } } } diff --git a/apct-tests/perftests/core/src/android/libcore/regression/CollectionsPerfTest.java b/apct-tests/perftests/core/src/android/libcore/regression/CollectionsPerfTest.java index a89efffcdd1f..69197c3325f4 100644 --- a/apct-tests/perftests/core/src/android/libcore/regression/CollectionsPerfTest.java +++ b/apct-tests/perftests/core/src/android/libcore/regression/CollectionsPerfTest.java @@ -20,11 +20,12 @@ import android.perftests.utils.BenchmarkState; import android.perftests.utils.PerfStatusReporter; import android.test.suitebuilder.annotation.LargeTest; +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; + import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameters; import java.util.ArrayList; import java.util.Arrays; @@ -35,19 +36,15 @@ import java.util.List; import java.util.Random; import java.util.Vector; -@RunWith(Parameterized.class) +@RunWith(JUnitParamsRunner.class) @LargeTest public class CollectionsPerfTest { @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); - @Parameters(name = "mArrayListLength({0})") - public static Collection<Object[]> data() { + public static Collection<Object[]> getData() { return Arrays.asList(new Object[][] {{4}, {16}, {64}, {256}, {1024}}); } - @Parameterized.Parameter(0) - public int arrayListLength; - public static Comparator<Integer> REVERSE = new Comparator<Integer>() { @Override @@ -59,7 +56,8 @@ public class CollectionsPerfTest { }; @Test - public void timeSort_arrayList() throws Exception { + @Parameters(method = "getData") + public void timeSort_arrayList(int arrayListLength) throws Exception { List<Integer> input = buildList(arrayListLength, ArrayList.class); BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { @@ -68,7 +66,8 @@ public class CollectionsPerfTest { } @Test - public void timeSortWithComparator_arrayList() throws Exception { + @Parameters(method = "getData") + public void timeSortWithComparator_arrayList(int arrayListLength) throws Exception { List<Integer> input = buildList(arrayListLength, ArrayList.class); BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { @@ -77,7 +76,8 @@ public class CollectionsPerfTest { } @Test - public void timeSort_vector() throws Exception { + @Parameters(method = "getData") + public void timeSort_vector(int arrayListLength) throws Exception { List<Integer> input = buildList(arrayListLength, Vector.class); BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { @@ -86,7 +86,8 @@ public class CollectionsPerfTest { } @Test - public void timeSortWithComparator_vector() throws Exception { + @Parameters(method = "getData") + public void timeSortWithComparator_vector(int arrayListLength) throws Exception { List<Integer> input = buildList(arrayListLength, Vector.class); BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { diff --git a/apct-tests/perftests/core/src/android/libcore/regression/EqualsHashCodePerfTest.java b/apct-tests/perftests/core/src/android/libcore/regression/EqualsHashCodePerfTest.java index 4ff3ba5b0aaa..839120336697 100644 --- a/apct-tests/perftests/core/src/android/libcore/regression/EqualsHashCodePerfTest.java +++ b/apct-tests/perftests/core/src/android/libcore/regression/EqualsHashCodePerfTest.java @@ -20,11 +20,12 @@ import android.perftests.utils.BenchmarkState; import android.perftests.utils.PerfStatusReporter; import android.test.suitebuilder.annotation.LargeTest; -import org.junit.Before; +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; + import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; import java.net.URI; import java.net.URL; @@ -32,39 +33,39 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; -@RunWith(Parameterized.class) +@RunWith(JUnitParamsRunner.class) @LargeTest public final class EqualsHashCodePerfTest { @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); private enum Type { URI() { - @Override Object newInstance(String text) throws Exception { + @Override + Object newInstance(String text) throws Exception { return new URI(text); } }, URL() { - @Override Object newInstance(String text) throws Exception { + @Override + Object newInstance(String text) throws Exception { return new URL(text); } }; + abstract Object newInstance(String text) throws Exception; } - private static final String QUERY = "%E0%AE%A8%E0%AE%BE%E0%AE%AE%E0%AF%8D+%E0%AE%AE%E0%AF%81%E0%AE%95%E0%AF%8D%E0%AE%95%E0%AE%BF%E0%AE%AF%E0%AE%AE%E0%AE%BE%E0%AE%A9%2C+%E0%AE%9A%E0%AF%81%E0%AE%B5%E0%AE%BE%E0%AE%B0%E0%AE%B8%E0%AF%8D%E0%AE%AF%E0%AE%AE%E0%AE%BE%E0%AE%A9+%E0%AE%87%E0%AE%B0%E0%AF%81%E0%AE%AA%E0%AF%8D%E0%AE%AA%E0%AF%87%E0%AE%BE%E0%AE%AE%E0%AF%8D%2C+%E0%AE%86%E0%AE%A9%E0%AE%BE%E0%AE%B2%E0%AF%8D+%E0%AE%9A%E0%AE%BF%E0%AE%B2+%E0%AE%A8%E0%AF%87%E0%AE%B0%E0%AE%99%E0%AF%8D%E0%AE%95%E0%AE%B3%E0%AE%BF%E0%AE%B2%E0%AF%8D+%E0%AE%9A%E0%AF%82%E0%AE%B4%E0%AF%8D%E0%AE%A8%E0%AE%BF%E0%AE%B2%E0%AF%88+%E0%AE%8F%E0%AE%B1%E0%AF%8D%E0%AE%AA%E0%AE%9F%E0%AF%81%E0%AE%AE%E0%AF%8D+%E0%AE%8E%E0%AE%A9%E0%AF%8D%E0%AE%AA%E0%AE%A4%E0%AE%BE%E0%AE%B2%E0%AF%8D+%E0%AE%AA%E0%AE%A3%E0%AE%BF%E0%AE%AF%E0%AF%88%E0%AE%AF%E0%AF%81%E0%AE%AE%E0%AF%8D+%E0%AE%B5%E0%AE%B2%E0%AE%BF+%E0%AE%85%E0%AE%B5%E0%AE%B0%E0%AF%88+%E0%AE%9A%E0%AE%BF%E0%AE%B2+%E0%AE%AA%E0%AF%86%E0%AE%B0%E0%AE%BF%E0%AE%AF+%E0%AE%95%E0%AF%86%E0%AE%BE%E0%AE%B3%E0%AF%8D%E0%AE%AE%E0%AF%81%E0%AE%A4%E0%AE%B2%E0%AF%8D+%E0%AE%AE%E0%AF%81%E0%AE%9F%E0%AE%BF%E0%AE%AF%E0%AF%81%E0%AE%AE%E0%AF%8D.+%E0%AE%85%E0%AE%A4%E0%AF%81+%E0%AE%9A%E0%AE%BF%E0%AE%B2+%E0%AE%A8%E0%AE%A9%E0%AF%8D%E0%AE%AE%E0%AF%88%E0%AE%95%E0%AE%B3%E0%AF%88+%E0%AE%AA%E0%AF%86%E0%AE%B1+%E0%AE%A4%E0%AE%B5%E0%AE%BF%E0%AE%B0%2C+%E0%AE%8E%E0%AE%AA%E0%AF%8D%E0%AE%AA%E0%AF%87%E0%AE%BE%E0%AE%A4%E0%AF%81%E0%AE%AE%E0%AF%8D+%E0%AE%89%E0%AE%B4%E0%AF%88%E0%AE%95%E0%AF%8D%E0%AE%95+%E0%AE%89%E0%AE%9F%E0%AE%B1%E0%AF%8D%E0%AE%AA%E0%AE%AF%E0%AE%BF%E0%AE%B1%E0%AF%8D%E0%AE%9A%E0%AE%BF+%E0%AE%AE%E0%AF%87%E0%AE%B1%E0%AF%8D%E0%AE%95%E0%AF%86%E0%AE%BE%E0%AE%B3%E0%AF%8D%E0%AE%95%E0%AE%BF%E0%AE%B1%E0%AE%A4%E0%AF%81+%E0%AE%8E%E0%AE%99%E0%AF%8D%E0%AE%95%E0%AE%B3%E0%AF%81%E0%AE%95%E0%AF%8D%E0%AE%95%E0%AF%81+%E0%AE%87%E0%AE%A4%E0%AF%81+%E0%AE%92%E0%AE%B0%E0%AF%81+%E0%AE%9A%E0%AE%BF%E0%AE%B1%E0%AE%BF%E0%AE%AF+%E0%AE%89%E0%AE%A4%E0%AE%BE%E0%AE%B0%E0%AE%A3%E0%AE%AE%E0%AF%8D%2C+%E0%AE%8E%E0%AE%9F%E0%AF%81%E0%AE%95%E0%AF%8D%E0%AE%95.+%E0%AE%B0%E0%AE%AF%E0%AE%BF%E0%AE%B2%E0%AF%8D+%E0%AE%8E%E0%AE%A8%E0%AF%8D%E0%AE%A4+%E0%AE%B5%E0%AE%BF%E0%AE%B3%E0%AF%88%E0%AE%B5%E0%AE%BE%E0%AE%95+%E0%AE%87%E0%AE%A9%E0%AF%8D%E0%AE%AA%E0%AE%AE%E0%AF%8D+%E0%AE%86%E0%AE%A9%E0%AF%8D%E0%AE%B2%E0%AF%88%E0%AE%A9%E0%AF%8D+%E0%AE%AA%E0%AE%AF%E0%AE%A9%E0%AF%8D%E0%AE%AA%E0%AE%BE%E0%AE%9F%E0%AF%81%E0%AE%95%E0%AE%B3%E0%AF%8D+%E0%AE%87%E0%AE%B0%E0%AF%81%E0%AE%95%E0%AF%8D%E0%AE%95+%E0%AE%B5%E0%AF%87%E0%AE%A3%E0%AF%8D%E0%AE%9F%E0%AF%81%E0%AE%AE%E0%AF%8D+%E0%AE%A4%E0%AE%AF%E0%AE%BE%E0%AE%B0%E0%AE%BF%E0%AE%95%E0%AF%8D%E0%AE%95%E0%AF%81%E0%AE%AE%E0%AF%8D+%E0%AE%A4%E0%AE%B5%E0%AE%B1%E0%AF%81+%E0%AE%95%E0%AE%A3%E0%AF%8D%E0%AE%9F%E0%AF%81%E0%AE%AA%E0%AE%BF%E0%AE%9F%E0%AE%BF%E0%AE%95%E0%AF%8D%E0%AE%95+%E0%AE%B5%E0%AE%B0%E0%AF%81%E0%AE%AE%E0%AF%8D+%E0%AE%A8%E0%AE%BE%E0%AE%AE%E0%AF%8D+%E0%AE%A4%E0%AE%B1%E0%AF%8D%E0%AE%AA%E0%AF%87%E0%AE%BE%E0%AE%A4%E0%AF%81+%E0%AE%87%E0%AE%B0%E0%AF%81%E0%AE%95%E0%AF%8D%E0%AE%95%E0%AE%BF%E0%AE%B1%E0%AF%87%E0%AE%BE%E0%AE%AE%E0%AF%8D.+%E0%AE%87%E0%AE%A8%E0%AF%8D%E0%AE%A4+%E0%AE%A8%E0%AE%BF%E0%AE%95%E0%AE%B4%E0%AF%8D%E0%AE%B5%E0%AF%81%E0%AE%95%E0%AE%B3%E0%AE%BF%E0%AE%B2%E0%AF%8D+%E0%AE%9A%E0%AF%86%E0%AE%AF%E0%AF%8D%E0%AE%A4%E0%AE%AA%E0%AE%BF%E0%AE%A9%E0%AF%8D+%E0%AE%85%E0%AE%AE%E0%AF%88%E0%AE%AA%E0%AF%8D%E0%AE%AA%E0%AE%BF%E0%AE%A9%E0%AF%8D+%E0%AE%95%E0%AE%A3%E0%AE%95%E0%AF%8D%E0%AE%95%E0%AF%81%2C+%E0%AE%85%E0%AE%B5%E0%AE%B0%E0%AF%8D%E0%AE%95%E0%AE%B3%E0%AF%8D+%E0%AE%A4%E0%AE%B5%E0%AE%B1%E0%AF%81+%E0%AE%B5%E0%AE%BF%E0%AE%9F%E0%AF%8D%E0%AE%9F%E0%AF%81+quae+%E0%AE%AA%E0%AE%9F%E0%AF%8D%E0%AE%9F%E0%AE%B1%E0%AF%88+%E0%AE%A8%E0%AF%80%E0%AE%99%E0%AF%8D%E0%AE%95%E0%AE%B3%E0%AF%8D+%E0%AE%AA%E0%AE%B0%E0%AE%BF%E0%AE%A8%E0%AF%8D%E0%AE%A4%E0%AF%81%E0%AE%B0%E0%AF%88%E0%AE%95%E0%AF%8D%E0%AE%95%E0%AE%BF%E0%AE%B1%E0%AF%87%E0%AE%BE%E0%AE%AE%E0%AF%8D+%E0%AE%AE%E0%AF%86%E0%AE%A9%E0%AF%8D%E0%AE%AE%E0%AF%88%E0%AE%AF%E0%AE%BE%E0%AE%95+%E0%AE%AE%E0%AE%BE%E0%AE%B1%E0%AF%81%E0%AE%AE%E0%AF%8D"; + private static final String QUERY = + "%E0%AE%A8%E0%AE%BE%E0%AE%AE%E0%AF%8D+%E0%AE%AE%E0%AF%81%E0%AE%95%E0%AF%8D%E0%AE%95%E0%AE%BF%E0%AE%AF%E0%AE%AE%E0%AE%BE%E0%AE%A9%2C+%E0%AE%9A%E0%AF%81%E0%AE%B5%E0%AE%BE%E0%AE%B0%E0%AE%B8%E0%AF%8D%E0%AE%AF%E0%AE%AE%E0%AE%BE%E0%AE%A9+%E0%AE%87%E0%AE%B0%E0%AF%81%E0%AE%AA%E0%AF%8D%E0%AE%AA%E0%AF%87%E0%AE%BE%E0%AE%AE%E0%AF%8D%2C+%E0%AE%86%E0%AE%A9%E0%AE%BE%E0%AE%B2%E0%AF%8D+%E0%AE%9A%E0%AE%BF%E0%AE%B2+%E0%AE%A8%E0%AF%87%E0%AE%B0%E0%AE%99%E0%AF%8D%E0%AE%95%E0%AE%B3%E0%AE%BF%E0%AE%B2%E0%AF%8D+%E0%AE%9A%E0%AF%82%E0%AE%B4%E0%AF%8D%E0%AE%A8%E0%AE%BF%E0%AE%B2%E0%AF%88+%E0%AE%8F%E0%AE%B1%E0%AF%8D%E0%AE%AA%E0%AE%9F%E0%AF%81%E0%AE%AE%E0%AF%8D+%E0%AE%8E%E0%AE%A9%E0%AF%8D%E0%AE%AA%E0%AE%A4%E0%AE%BE%E0%AE%B2%E0%AF%8D+%E0%AE%AA%E0%AE%A3%E0%AE%BF%E0%AE%AF%E0%AF%88%E0%AE%AF%E0%AF%81%E0%AE%AE%E0%AF%8D+%E0%AE%B5%E0%AE%B2%E0%AE%BF+%E0%AE%85%E0%AE%B5%E0%AE%B0%E0%AF%88+%E0%AE%9A%E0%AE%BF%E0%AE%B2+%E0%AE%AA%E0%AF%86%E0%AE%B0%E0%AE%BF%E0%AE%AF+%E0%AE%95%E0%AF%86%E0%AE%BE%E0%AE%B3%E0%AF%8D%E0%AE%AE%E0%AF%81%E0%AE%A4%E0%AE%B2%E0%AF%8D+%E0%AE%AE%E0%AF%81%E0%AE%9F%E0%AE%BF%E0%AE%AF%E0%AF%81%E0%AE%AE%E0%AF%8D.+%E0%AE%85%E0%AE%A4%E0%AF%81+%E0%AE%9A%E0%AE%BF%E0%AE%B2+%E0%AE%A8%E0%AE%A9%E0%AF%8D%E0%AE%AE%E0%AF%88%E0%AE%95%E0%AE%B3%E0%AF%88+%E0%AE%AA%E0%AF%86%E0%AE%B1+%E0%AE%A4%E0%AE%B5%E0%AE%BF%E0%AE%B0%2C+%E0%AE%8E%E0%AE%AA%E0%AF%8D%E0%AE%AA%E0%AF%87%E0%AE%BE%E0%AE%A4%E0%AF%81%E0%AE%AE%E0%AF%8D+%E0%AE%89%E0%AE%B4%E0%AF%88%E0%AE%95%E0%AF%8D%E0%AE%95+%E0%AE%89%E0%AE%9F%E0%AE%B1%E0%AF%8D%E0%AE%AA%E0%AE%AF%E0%AE%BF%E0%AE%B1%E0%AF%8D%E0%AE%9A%E0%AE%BF+%E0%AE%AE%E0%AF%87%E0%AE%B1%E0%AF%8D%E0%AE%95%E0%AF%86%E0%AE%BE%E0%AE%B3%E0%AF%8D%E0%AE%95%E0%AE%BF%E0%AE%B1%E0%AE%A4%E0%AF%81+%E0%AE%8E%E0%AE%99%E0%AF%8D%E0%AE%95%E0%AE%B3%E0%AF%81%E0%AE%95%E0%AF%8D%E0%AE%95%E0%AF%81+%E0%AE%87%E0%AE%A4%E0%AF%81+%E0%AE%92%E0%AE%B0%E0%AF%81+%E0%AE%9A%E0%AE%BF%E0%AE%B1%E0%AE%BF%E0%AE%AF+%E0%AE%89%E0%AE%A4%E0%AE%BE%E0%AE%B0%E0%AE%A3%E0%AE%AE%E0%AF%8D%2C+%E0%AE%8E%E0%AE%9F%E0%AF%81%E0%AE%95%E0%AF%8D%E0%AE%95.+%E0%AE%B0%E0%AE%AF%E0%AE%BF%E0%AE%B2%E0%AF%8D+%E0%AE%8E%E0%AE%A8%E0%AF%8D%E0%AE%A4+%E0%AE%B5%E0%AE%BF%E0%AE%B3%E0%AF%88%E0%AE%B5%E0%AE%BE%E0%AE%95+%E0%AE%87%E0%AE%A9%E0%AF%8D%E0%AE%AA%E0%AE%AE%E0%AF%8D+%E0%AE%86%E0%AE%A9%E0%AF%8D%E0%AE%B2%E0%AF%88%E0%AE%A9%E0%AF%8D+%E0%AE%AA%E0%AE%AF%E0%AE%A9%E0%AF%8D%E0%AE%AA%E0%AE%BE%E0%AE%9F%E0%AF%81%E0%AE%95%E0%AE%B3%E0%AF%8D+%E0%AE%87%E0%AE%B0%E0%AF%81%E0%AE%95%E0%AF%8D%E0%AE%95+%E0%AE%B5%E0%AF%87%E0%AE%A3%E0%AF%8D%E0%AE%9F%E0%AF%81%E0%AE%AE%E0%AF%8D+%E0%AE%A4%E0%AE%AF%E0%AE%BE%E0%AE%B0%E0%AE%BF%E0%AE%95%E0%AF%8D%E0%AE%95%E0%AF%81%E0%AE%AE%E0%AF%8D+%E0%AE%A4%E0%AE%B5%E0%AE%B1%E0%AF%81+%E0%AE%95%E0%AE%A3%E0%AF%8D%E0%AE%9F%E0%AF%81%E0%AE%AA%E0%AE%BF%E0%AE%9F%E0%AE%BF%E0%AE%95%E0%AF%8D%E0%AE%95+%E0%AE%B5%E0%AE%B0%E0%AF%81%E0%AE%AE%E0%AF%8D+%E0%AE%A8%E0%AE%BE%E0%AE%AE%E0%AF%8D+%E0%AE%A4%E0%AE%B1%E0%AF%8D%E0%AE%AA%E0%AF%87%E0%AE%BE%E0%AE%A4%E0%AF%81+%E0%AE%87%E0%AE%B0%E0%AF%81%E0%AE%95%E0%AF%8D%E0%AE%95%E0%AE%BF%E0%AE%B1%E0%AF%87%E0%AE%BE%E0%AE%AE%E0%AF%8D.+%E0%AE%87%E0%AE%A8%E0%AF%8D%E0%AE%A4+%E0%AE%A8%E0%AE%BF%E0%AE%95%E0%AE%B4%E0%AF%8D%E0%AE%B5%E0%AF%81%E0%AE%95%E0%AE%B3%E0%AE%BF%E0%AE%B2%E0%AF%8D+%E0%AE%9A%E0%AF%86%E0%AE%AF%E0%AF%8D%E0%AE%A4%E0%AE%AA%E0%AE%BF%E0%AE%A9%E0%AF%8D+%E0%AE%85%E0%AE%AE%E0%AF%88%E0%AE%AA%E0%AF%8D%E0%AE%AA%E0%AE%BF%E0%AE%A9%E0%AF%8D+%E0%AE%95%E0%AE%A3%E0%AE%95%E0%AF%8D%E0%AE%95%E0%AF%81%2C+%E0%AE%85%E0%AE%B5%E0%AE%B0%E0%AF%8D%E0%AE%95%E0%AE%B3%E0%AF%8D+%E0%AE%A4%E0%AE%B5%E0%AE%B1%E0%AF%81+%E0%AE%B5%E0%AE%BF%E0%AE%9F%E0%AF%8D%E0%AE%9F%E0%AF%81+quae+%E0%AE%AA%E0%AE%9F%E0%AF%8D%E0%AE%9F%E0%AE%B1%E0%AF%88+%E0%AE%A8%E0%AF%80%E0%AE%99%E0%AF%8D%E0%AE%95%E0%AE%B3%E0%AF%8D+%E0%AE%AA%E0%AE%B0%E0%AE%BF%E0%AE%A8%E0%AF%8D%E0%AE%A4%E0%AF%81%E0%AE%B0%E0%AF%88%E0%AE%95%E0%AF%8D%E0%AE%95%E0%AE%BF%E0%AE%B1%E0%AF%87%E0%AE%BE%E0%AE%AE%E0%AF%8D+%E0%AE%AE%E0%AF%86%E0%AE%A9%E0%AF%8D%E0%AE%AE%E0%AF%88%E0%AE%AF%E0%AE%BE%E0%AE%95+%E0%AE%AE%E0%AE%BE%E0%AE%B1%E0%AF%81%E0%AE%AE%E0%AF%8D"; - @Parameterized.Parameters(name = "mType({0})") - public static Collection cases() { + public static Collection getCases() { final List<Object[]> params = new ArrayList<>(); for (Type type : Type.values()) { - params.add(new Object[]{type}); + params.add(new Object[] {type}); } return params; } - @Parameterized.Parameter(0) - public Type mType; - Object mA1; Object mA2; Object mB1; @@ -73,20 +74,13 @@ public final class EqualsHashCodePerfTest { Object mC1; Object mC2; - @Before - public void setUp() throws Exception { - mA1 = mType.newInstance("https://mail.google.com/mail/u/0/?shva=1#inbox"); - mA2 = mType.newInstance("https://mail.google.com/mail/u/0/?shva=1#inbox"); - mB1 = mType.newInstance("http://developer.android.com/reference/java/net/URI.html"); - mB2 = mType.newInstance("http://developer.android.com/reference/java/net/URI.html"); - - mC1 = mType.newInstance("http://developer.android.com/query?q=" + QUERY); - // Replace the very last char. - mC2 = mType.newInstance("http://developer.android.com/query?q=" + QUERY.substring(0, QUERY.length() - 3) + "%AF"); - } - @Test - public void timeEquals() { + @Parameters(method = "getCases") + public void timeEquals(Type type) throws Exception { + mA1 = type.newInstance("https://mail.google.com/mail/u/0/?shva=1#inbox"); + mA2 = type.newInstance("https://mail.google.com/mail/u/0/?shva=1#inbox"); + mB1 = type.newInstance("http://developer.android.com/reference/java/net/URI.html"); + mB2 = type.newInstance("http://developer.android.com/reference/java/net/URI.html"); BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { mA1.equals(mB1); @@ -96,7 +90,10 @@ public final class EqualsHashCodePerfTest { } @Test - public void timeHashCode() { + @Parameters(method = "getCases") + public void timeHashCode(Type type) throws Exception { + mA1 = type.newInstance("https://mail.google.com/mail/u/0/?shva=1#inbox"); + mB1 = type.newInstance("http://developer.android.com/reference/java/net/URI.html"); BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { mA1.hashCode(); @@ -105,7 +102,15 @@ public final class EqualsHashCodePerfTest { } @Test - public void timeEqualsWithHeavilyEscapedComponent() { + @Parameters(method = "getCases") + public void timeEqualsWithHeavilyEscapedComponent(Type type) throws Exception { + mC1 = type.newInstance("http://developer.android.com/query?q=" + QUERY); + // Replace the very last char. + mC2 = + type.newInstance( + "http://developer.android.com/query?q=" + + QUERY.substring(0, QUERY.length() - 3) + + "%AF"); BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { mC1.equals(mC2); diff --git a/apct-tests/perftests/core/src/android/libcore/regression/KeyPairGeneratorPerfTest.java b/apct-tests/perftests/core/src/android/libcore/regression/KeyPairGeneratorPerfTest.java index 6fe9059cb3de..80c448732b59 100644 --- a/apct-tests/perftests/core/src/android/libcore/regression/KeyPairGeneratorPerfTest.java +++ b/apct-tests/perftests/core/src/android/libcore/regression/KeyPairGeneratorPerfTest.java @@ -20,26 +20,24 @@ import android.perftests.utils.BenchmarkState; import android.perftests.utils.PerfStatusReporter; import android.test.suitebuilder.annotation.LargeTest; -import org.junit.Before; +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; + import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameters; import java.security.KeyPair; import java.security.KeyPairGenerator; -import java.security.SecureRandom; import java.util.Arrays; import java.util.Collection; -@RunWith(Parameterized.class) +@RunWith(JUnitParamsRunner.class) @LargeTest public class KeyPairGeneratorPerfTest { @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); - @Parameters(name = "mAlgorithm={0}, mImplementation={1}") - public static Collection<Object[]> data() { + public static Collection<Object[]> getData() { return Arrays.asList( new Object[][] { {Algorithm.RSA, Implementation.BouncyCastle}, @@ -48,12 +46,6 @@ public class KeyPairGeneratorPerfTest { }); } - @Parameterized.Parameter(0) - public Algorithm mAlgorithm; - - @Parameterized.Parameter(1) - public Implementation mImplementation; - public enum Algorithm { RSA, DSA, @@ -66,26 +58,25 @@ public class KeyPairGeneratorPerfTest { private String mGeneratorAlgorithm; private KeyPairGenerator mGenerator; - private SecureRandom mRandom; - @Before - public void setUp() throws Exception { - this.mGeneratorAlgorithm = mAlgorithm.toString(); + public void setUp(Algorithm algorithm, Implementation implementation) throws Exception { + this.mGeneratorAlgorithm = algorithm.toString(); final String provider; - if (mImplementation == Implementation.BouncyCastle) { + if (implementation == Implementation.BouncyCastle) { provider = "BC"; } else { provider = "AndroidOpenSSL"; } this.mGenerator = KeyPairGenerator.getInstance(mGeneratorAlgorithm, provider); - this.mRandom = SecureRandom.getInstance("SHA1PRNG"); this.mGenerator.initialize(1024); } @Test - public void time() throws Exception { + @Parameters(method = "getData") + public void time(Algorithm algorithm, Implementation implementation) throws Exception { + setUp(algorithm, implementation); BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { KeyPair keyPair = mGenerator.generateKeyPair(); diff --git a/apct-tests/perftests/core/src/android/libcore/regression/LoopingBackwardsPerfTest.java b/apct-tests/perftests/core/src/android/libcore/regression/LoopingBackwardsPerfTest.java index 414764d292b8..c9b0cbe1bedb 100644 --- a/apct-tests/perftests/core/src/android/libcore/regression/LoopingBackwardsPerfTest.java +++ b/apct-tests/perftests/core/src/android/libcore/regression/LoopingBackwardsPerfTest.java @@ -20,11 +20,12 @@ import android.perftests.utils.BenchmarkState; import android.perftests.utils.PerfStatusReporter; import android.test.suitebuilder.annotation.LargeTest; +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; + import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameters; import java.util.Arrays; import java.util.Collection; @@ -34,36 +35,34 @@ import java.util.Collection; * * @author Kevin Bourrillion */ -@RunWith(Parameterized.class) +@RunWith(JUnitParamsRunner.class) @LargeTest public class LoopingBackwardsPerfTest { @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); - @Parameters(name = "mMax={0}") - public static Collection<Object[]> data() { + public static Collection<Object[]> getData() { return Arrays.asList(new Object[][] {{2}, {20}, {2000}, {20000000}}); } - @Parameterized.Parameter(0) - public int mMax; - @Test - public void timeForwards() { + @Parameters(method = "getData") + public void timeForwards(int max) { int fake = 0; BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { - for (int j = 0; j < mMax; j++) { + for (int j = 0; j < max; j++) { fake += j; } } } @Test - public void timeBackwards() { + @Parameters(method = "getData") + public void timeBackwards(int max) { int fake = 0; BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { - for (int j = mMax - 1; j >= 0; j--) { + for (int j = max - 1; j >= 0; j--) { fake += j; } } diff --git a/apct-tests/perftests/core/src/android/libcore/regression/MessageDigestPerfTest.java b/apct-tests/perftests/core/src/android/libcore/regression/MessageDigestPerfTest.java index 279681bc0d15..2dc947a613d2 100644 --- a/apct-tests/perftests/core/src/android/libcore/regression/MessageDigestPerfTest.java +++ b/apct-tests/perftests/core/src/android/libcore/regression/MessageDigestPerfTest.java @@ -20,24 +20,24 @@ import android.perftests.utils.BenchmarkState; import android.perftests.utils.PerfStatusReporter; import android.test.suitebuilder.annotation.LargeTest; +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; + import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameters; import java.nio.ByteBuffer; import java.security.MessageDigest; import java.util.Arrays; import java.util.Collection; -@RunWith(Parameterized.class) +@RunWith(JUnitParamsRunner.class) @LargeTest public class MessageDigestPerfTest { @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); - @Parameters(name = "mAlgorithm={0}") - public static Collection<Object[]> data() { + public static Collection<Object[]> getData() { return Arrays.asList( new Object[][] { {Algorithm.MD5}, @@ -48,9 +48,6 @@ public class MessageDigestPerfTest { }); } - @Parameterized.Parameter(0) - public Algorithm mAlgorithm; - public String mProvider = "AndroidOpenSSL"; private static final int DATA_SIZE = 8192; @@ -97,44 +94,44 @@ public class MessageDigestPerfTest { }; @Test - public void time() throws Exception { + @Parameters(method = "getData") + public void time(Algorithm algorithm) throws Exception { BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { - MessageDigest digest = - MessageDigest.getInstance(mAlgorithm.toString(), mProvider); + MessageDigest digest = MessageDigest.getInstance(algorithm.toString(), mProvider); digest.update(DATA, 0, DATA_SIZE); digest.digest(); } } @Test - public void timeLargeArray() throws Exception { + @Parameters(method = "getData") + public void timeLargeArray(Algorithm algorithm) throws Exception { BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { - MessageDigest digest = - MessageDigest.getInstance(mAlgorithm.toString(), mProvider); + MessageDigest digest = MessageDigest.getInstance(algorithm.toString(), mProvider); digest.update(LARGE_DATA, 0, LARGE_DATA_SIZE); digest.digest(); } } @Test - public void timeSmallChunkOfLargeArray() throws Exception { + @Parameters(method = "getData") + public void timeSmallChunkOfLargeArray(Algorithm algorithm) throws Exception { BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { - MessageDigest digest = - MessageDigest.getInstance(mAlgorithm.toString(), mProvider); + MessageDigest digest = MessageDigest.getInstance(algorithm.toString(), mProvider); digest.update(LARGE_DATA, LARGE_DATA_SIZE / 2, DATA_SIZE); digest.digest(); } } @Test - public void timeSmallByteBuffer() throws Exception { + @Parameters(method = "getData") + public void timeSmallByteBuffer(Algorithm algorithm) throws Exception { BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { - MessageDigest digest = - MessageDigest.getInstance(mAlgorithm.toString(), mProvider); + MessageDigest digest = MessageDigest.getInstance(algorithm.toString(), mProvider); SMALL_BUFFER.position(0); SMALL_BUFFER.limit(SMALL_BUFFER.capacity()); digest.update(SMALL_BUFFER); @@ -143,11 +140,11 @@ public class MessageDigestPerfTest { } @Test - public void timeSmallDirectByteBuffer() throws Exception { + @Parameters(method = "getData") + public void timeSmallDirectByteBuffer(Algorithm algorithm) throws Exception { BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { - MessageDigest digest = - MessageDigest.getInstance(mAlgorithm.toString(), mProvider); + MessageDigest digest = MessageDigest.getInstance(algorithm.toString(), mProvider); SMALL_DIRECT_BUFFER.position(0); SMALL_DIRECT_BUFFER.limit(SMALL_DIRECT_BUFFER.capacity()); digest.update(SMALL_DIRECT_BUFFER); @@ -156,11 +153,11 @@ public class MessageDigestPerfTest { } @Test - public void timeLargeByteBuffer() throws Exception { + @Parameters(method = "getData") + public void timeLargeByteBuffer(Algorithm algorithm) throws Exception { BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { - MessageDigest digest = - MessageDigest.getInstance(mAlgorithm.toString(), mProvider); + MessageDigest digest = MessageDigest.getInstance(algorithm.toString(), mProvider); LARGE_BUFFER.position(0); LARGE_BUFFER.limit(LARGE_BUFFER.capacity()); digest.update(LARGE_BUFFER); @@ -169,11 +166,11 @@ public class MessageDigestPerfTest { } @Test - public void timeLargeDirectByteBuffer() throws Exception { + @Parameters(method = "getData") + public void timeLargeDirectByteBuffer(Algorithm algorithm) throws Exception { BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { - MessageDigest digest = - MessageDigest.getInstance(mAlgorithm.toString(), mProvider); + MessageDigest digest = MessageDigest.getInstance(algorithm.toString(), mProvider); LARGE_DIRECT_BUFFER.position(0); LARGE_DIRECT_BUFFER.limit(LARGE_DIRECT_BUFFER.capacity()); digest.update(LARGE_DIRECT_BUFFER); @@ -182,11 +179,11 @@ public class MessageDigestPerfTest { } @Test - public void timeSmallChunkOfLargeByteBuffer() throws Exception { + @Parameters(method = "getData") + public void timeSmallChunkOfLargeByteBuffer(Algorithm algorithm) throws Exception { BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { - MessageDigest digest = - MessageDigest.getInstance(mAlgorithm.toString(), mProvider); + MessageDigest digest = MessageDigest.getInstance(algorithm.toString(), mProvider); LARGE_BUFFER.position(LARGE_BUFFER.capacity() / 2); LARGE_BUFFER.limit(LARGE_BUFFER.position() + DATA_SIZE); digest.update(LARGE_BUFFER); @@ -195,11 +192,11 @@ public class MessageDigestPerfTest { } @Test - public void timeSmallChunkOfLargeDirectByteBuffer() throws Exception { + @Parameters(method = "getData") + public void timeSmallChunkOfLargeDirectByteBuffer(Algorithm algorithm) throws Exception { BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { - MessageDigest digest = - MessageDigest.getInstance(mAlgorithm.toString(), mProvider); + MessageDigest digest = MessageDigest.getInstance(algorithm.toString(), mProvider); LARGE_DIRECT_BUFFER.position(LARGE_DIRECT_BUFFER.capacity() / 2); LARGE_DIRECT_BUFFER.limit(LARGE_DIRECT_BUFFER.position() + DATA_SIZE); digest.update(LARGE_DIRECT_BUFFER); diff --git a/apct-tests/perftests/core/src/android/libcore/regression/MutableIntPerfTest.java b/apct-tests/perftests/core/src/android/libcore/regression/MutableIntPerfTest.java index 37bd73c8731a..d9d4bb5d0ae1 100644 --- a/apct-tests/perftests/core/src/android/libcore/regression/MutableIntPerfTest.java +++ b/apct-tests/perftests/core/src/android/libcore/regression/MutableIntPerfTest.java @@ -20,17 +20,18 @@ import android.perftests.utils.BenchmarkState; import android.perftests.utils.PerfStatusReporter; import android.test.suitebuilder.annotation.LargeTest; +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; + import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameters; import java.util.Arrays; import java.util.Collection; import java.util.concurrent.atomic.AtomicInteger; -@RunWith(Parameterized.class) +@RunWith(JUnitParamsRunner.class) @LargeTest public final class MutableIntPerfTest { @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); @@ -96,29 +97,28 @@ public final class MutableIntPerfTest { abstract int timeGet(BenchmarkState state); } - @Parameters(name = "mKind={0}") - public static Collection<Object[]> data() { + public static Collection<Object[]> getData() { return Arrays.asList(new Object[][] {{Kind.ARRAY}, {Kind.ATOMIC}}); } - @Parameterized.Parameter(0) - public Kind mKind; - @Test - public void timeCreate() { + @Parameters(method = "getData") + public void timeCreate(Kind kind) { BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); - mKind.timeCreate(state); + kind.timeCreate(state); } @Test - public void timeIncrement() { + @Parameters(method = "getData") + public void timeIncrement(Kind kind) { BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); - mKind.timeIncrement(state); + kind.timeIncrement(state); } @Test - public void timeGet() { + @Parameters(method = "getData") + public void timeGet(Kind kind) { BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); - mKind.timeGet(state); + kind.timeGet(state); } } diff --git a/apct-tests/perftests/core/src/android/libcore/regression/PriorityQueuePerfTest.java b/apct-tests/perftests/core/src/android/libcore/regression/PriorityQueuePerfTest.java index 8801a5690cb2..48450b4616e6 100644 --- a/apct-tests/perftests/core/src/android/libcore/regression/PriorityQueuePerfTest.java +++ b/apct-tests/perftests/core/src/android/libcore/regression/PriorityQueuePerfTest.java @@ -20,12 +20,12 @@ import android.perftests.utils.BenchmarkState; import android.perftests.utils.PerfStatusReporter; import android.test.suitebuilder.annotation.LargeTest; -import org.junit.Before; +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; + import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameters; import java.util.ArrayList; import java.util.Arrays; @@ -35,13 +35,12 @@ import java.util.List; import java.util.PriorityQueue; import java.util.Random; -@RunWith(Parameterized.class) +@RunWith(JUnitParamsRunner.class) @LargeTest public class PriorityQueuePerfTest { @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); - @Parameters(name = "mQueueSize={0}, mHitRate={1}") - public static Collection<Object[]> data() { + public static Collection<Object[]> getData() { return Arrays.asList( new Object[][] { {100, 0}, @@ -62,26 +61,19 @@ public class PriorityQueuePerfTest { }); } - @Parameterized.Parameter(0) - public int mQueueSize; - - @Parameterized.Parameter(1) - public int mHitRate; - private PriorityQueue<Integer> mPq; private PriorityQueue<Integer> mUsepq; private List<Integer> mSeekElements; private Random mRandom = new Random(189279387L); - @Before - public void setUp() throws Exception { + public void setUp(int queueSize, int hitRate) throws Exception { mPq = new PriorityQueue<Integer>(); mUsepq = new PriorityQueue<Integer>(); mSeekElements = new ArrayList<Integer>(); List<Integer> allElements = new ArrayList<Integer>(); - int numShared = (int) (mQueueSize * ((double) mHitRate / 100)); - // the total number of elements we require to engineer a hit rate of mHitRate% - int totalElements = 2 * mQueueSize - numShared; + int numShared = (int) (queueSize * ((double) hitRate / 100)); + // the total number of elements we require to engineer a hit rate of hitRate% + int totalElements = 2 * queueSize - numShared; for (int i = 0; i < totalElements; i++) { allElements.add(i); } @@ -93,11 +85,11 @@ public class PriorityQueuePerfTest { mSeekElements.add(allElements.get(i)); } // add priority queue only elements (these won't be touched) - for (int i = numShared; i < mQueueSize; i++) { + for (int i = numShared; i < queueSize; i++) { mPq.add(allElements.get(i)); } // add non-priority queue elements (these will be misses) - for (int i = mQueueSize; i < totalElements; i++) { + for (int i = queueSize; i < totalElements; i++) { mSeekElements.add(allElements.get(i)); } mUsepq = new PriorityQueue<Integer>(mPq); @@ -107,16 +99,18 @@ public class PriorityQueuePerfTest { } @Test - public void timeRemove() { + @Parameters(method = "getData") + public void timeRemove(int queueSize, int hitRate) throws Exception { + setUp(queueSize, hitRate); boolean fake = false; int elementsSize = mSeekElements.size(); // At most allow the queue to empty 10%. - int resizingThreshold = mQueueSize / 10; + int resizingThreshold = queueSize / 10; int i = 0; BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { // Reset queue every so often. This will be called more often for smaller - // mQueueSizes, but since a copy is linear, it will also cost proportionally + // queueSizes, but since a copy is linear, it will also cost proportionally // less, and hopefully it will approximately balance out. if (++i % resizingThreshold == 0) { mUsepq = new PriorityQueue<Integer>(mPq); diff --git a/apct-tests/perftests/core/src/android/libcore/regression/SchemePrefixPerfTest.java b/apct-tests/perftests/core/src/android/libcore/regression/SchemePrefixPerfTest.java index 42dc5811e6db..5ad62dedcae7 100644 --- a/apct-tests/perftests/core/src/android/libcore/regression/SchemePrefixPerfTest.java +++ b/apct-tests/perftests/core/src/android/libcore/regression/SchemePrefixPerfTest.java @@ -20,11 +20,12 @@ import android.perftests.utils.BenchmarkState; import android.perftests.utils.PerfStatusReporter; import android.test.suitebuilder.annotation.LargeTest; +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; + import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameters; import java.util.Arrays; import java.util.Collection; @@ -32,7 +33,7 @@ import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; -@RunWith(Parameterized.class) +@RunWith(JUnitParamsRunner.class) @LargeTest public final class SchemePrefixPerfTest { @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); @@ -85,19 +86,16 @@ public final class SchemePrefixPerfTest { abstract String execute(String spec); } - @Parameters(name = "mStrategy={0}") - public static Collection<Object[]> data() { + public static Collection<Object[]> getData() { return Arrays.asList(new Object[][] {{Strategy.REGEX}, {Strategy.JAVA}}); } - @Parameterized.Parameter(0) - public Strategy mStrategy; - @Test - public void timeSchemePrefix() { + @Parameters(method = "getData") + public void timeSchemePrefix(Strategy strategy) { BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { - mStrategy.execute("http://android.com"); + strategy.execute("http://android.com"); } } } diff --git a/apct-tests/perftests/core/src/android/libcore/regression/SignaturePerfTest.java b/apct-tests/perftests/core/src/android/libcore/regression/SignaturePerfTest.java index 96e7cb27afef..a9a0788f6136 100644 --- a/apct-tests/perftests/core/src/android/libcore/regression/SignaturePerfTest.java +++ b/apct-tests/perftests/core/src/android/libcore/regression/SignaturePerfTest.java @@ -19,12 +19,12 @@ import android.perftests.utils.BenchmarkState; import android.perftests.utils.PerfStatusReporter; import android.test.suitebuilder.annotation.LargeTest; -import org.junit.Before; +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; + import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameters; import java.security.KeyPair; import java.security.KeyPairGenerator; @@ -37,13 +37,12 @@ import java.util.HashMap; import java.util.Map; /** Tests RSA and DSA mSignature creation and verification. */ -@RunWith(Parameterized.class) +@RunWith(JUnitParamsRunner.class) @LargeTest public class SignaturePerfTest { @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); - @Parameters(name = "mAlgorithm={0}, mImplementation={1}") - public static Collection<Object[]> data() { + public static Collection<Object[]> getData() { return Arrays.asList( new Object[][] { {Algorithm.MD5WithRSA, Implementation.OpenSSL}, @@ -55,12 +54,6 @@ public class SignaturePerfTest { }); } - @Parameterized.Parameter(0) - public Algorithm mAlgorithm; - - @Parameterized.Parameter(1) - public Implementation mImplementation; - private static final int DATA_SIZE = 8192; private static final byte[] DATA = new byte[DATA_SIZE]; @@ -94,9 +87,8 @@ public class SignaturePerfTest { private PrivateKey mPrivateKey; private PublicKey mPublicKey; - @Before - public void setUp() throws Exception { - this.mSignatureAlgorithm = mAlgorithm.toString(); + public void setUp(Algorithm algorithm) throws Exception { + this.mSignatureAlgorithm = algorithm.toString(); String keyAlgorithm = mSignatureAlgorithm.substring( @@ -121,11 +113,13 @@ public class SignaturePerfTest { } @Test - public void timeSign() throws Exception { + @Parameters(method = "getData") + public void timeSign(Algorithm algorithm, Implementation implementation) throws Exception { + setUp(algorithm); BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { Signature signer; - switch (mImplementation) { + switch (implementation) { case OpenSSL: signer = Signature.getInstance(mSignatureAlgorithm, "AndroidOpenSSL"); break; @@ -133,7 +127,7 @@ public class SignaturePerfTest { signer = Signature.getInstance(mSignatureAlgorithm, "BC"); break; default: - throw new RuntimeException(mImplementation.toString()); + throw new RuntimeException(implementation.toString()); } signer.initSign(mPrivateKey); signer.update(DATA); @@ -142,11 +136,13 @@ public class SignaturePerfTest { } @Test - public void timeVerify() throws Exception { + @Parameters(method = "getData") + public void timeVerify(Algorithm algorithm, Implementation implementation) throws Exception { + setUp(algorithm); BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { Signature verifier; - switch (mImplementation) { + switch (implementation) { case OpenSSL: verifier = Signature.getInstance(mSignatureAlgorithm, "AndroidOpenSSL"); break; @@ -154,7 +150,7 @@ public class SignaturePerfTest { verifier = Signature.getInstance(mSignatureAlgorithm, "BC"); break; default: - throw new RuntimeException(mImplementation.toString()); + throw new RuntimeException(implementation.toString()); } verifier.initVerify(mPublicKey); verifier.update(DATA); diff --git a/apct-tests/perftests/core/src/android/libcore/regression/StringPerfTest.java b/apct-tests/perftests/core/src/android/libcore/regression/StringPerfTest.java index 02194b1b9745..36db014b75a5 100644 --- a/apct-tests/perftests/core/src/android/libcore/regression/StringPerfTest.java +++ b/apct-tests/perftests/core/src/android/libcore/regression/StringPerfTest.java @@ -20,16 +20,17 @@ import android.perftests.utils.BenchmarkState; import android.perftests.utils.PerfStatusReporter; import android.test.suitebuilder.annotation.LargeTest; +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; + import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameters; import java.util.Arrays; import java.util.Collection; -@RunWith(Parameterized.class) +@RunWith(JUnitParamsRunner.class) @LargeTest public class StringPerfTest { @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); @@ -46,8 +47,7 @@ public class StringPerfTest { } } - @Parameters(name = "mStringLengths={0}") - public static Collection<Object[]> data() { + public static Collection<Object[]> getData() { return Arrays.asList( new Object[][] { {StringLengths.EIGHT_KI}, @@ -57,9 +57,6 @@ public class StringPerfTest { }); } - @Parameterized.Parameter(0) - public StringLengths mStringLengths; - private static String makeString(int length) { StringBuilder result = new StringBuilder(length); for (int i = 0; i < length; ++i) { @@ -69,10 +66,11 @@ public class StringPerfTest { } @Test - public void timeHashCode() { + @Parameters(method = "getData") + public void timeHashCode(StringLengths stringLengths) { BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { - mStringLengths.mValue.hashCode(); + stringLengths.mValue.hashCode(); } } } diff --git a/apct-tests/perftests/core/src/android/libcore/regression/StringReplaceAllPerfTest.java b/apct-tests/perftests/core/src/android/libcore/regression/StringReplaceAllPerfTest.java index b0d1ee4132fa..5b4423a32831 100644 --- a/apct-tests/perftests/core/src/android/libcore/regression/StringReplaceAllPerfTest.java +++ b/apct-tests/perftests/core/src/android/libcore/regression/StringReplaceAllPerfTest.java @@ -20,16 +20,17 @@ import android.perftests.utils.BenchmarkState; import android.perftests.utils.PerfStatusReporter; import android.test.suitebuilder.annotation.LargeTest; +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; + import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameters; import java.util.Arrays; import java.util.Collection; -@RunWith(Parameterized.class) +@RunWith(JUnitParamsRunner.class) @LargeTest public class StringReplaceAllPerfTest { @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); @@ -69,8 +70,7 @@ public class StringReplaceAllPerfTest { return stringBuilder.toString(); } - @Parameters(name = "mStringLengths={0}") - public static Collection<Object[]> data() { + public static Collection<Object[]> getData() { return Arrays.asList( new Object[][] { {StringLengths.BOOT_IMAGE}, @@ -82,30 +82,30 @@ public class StringReplaceAllPerfTest { }); } - @Parameterized.Parameter(0) - public StringLengths mStringLengths; - @Test - public void timeReplaceAllTrivialPatternNonExistent() { + @Parameters(method = "getData") + public void timeReplaceAllTrivialPatternNonExistent(StringLengths stringLengths) { BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { - mStringLengths.mValue.replaceAll("fish", "0"); + stringLengths.mValue.replaceAll("fish", "0"); } } @Test - public void timeReplaceTrivialPatternAllRepeated() { + @Parameters(method = "getData") + public void timeReplaceTrivialPatternAllRepeated(StringLengths stringLengths) { BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { - mStringLengths.mValue.replaceAll("jklm", "0"); + stringLengths.mValue.replaceAll("jklm", "0"); } } @Test - public void timeReplaceAllTrivialPatternSingleOccurrence() { + @Parameters(method = "getData") + public void timeReplaceAllTrivialPatternSingleOccurrence(StringLengths stringLengths) { BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { - mStringLengths.mValue.replaceAll("qrst", "0"); + stringLengths.mValue.replaceAll("qrst", "0"); } } } diff --git a/apct-tests/perftests/core/src/android/libcore/regression/StringReplacePerfTest.java b/apct-tests/perftests/core/src/android/libcore/regression/StringReplacePerfTest.java index d2e657a78608..4d5c792295b9 100644 --- a/apct-tests/perftests/core/src/android/libcore/regression/StringReplacePerfTest.java +++ b/apct-tests/perftests/core/src/android/libcore/regression/StringReplacePerfTest.java @@ -20,16 +20,17 @@ import android.perftests.utils.BenchmarkState; import android.perftests.utils.PerfStatusReporter; import android.test.suitebuilder.annotation.LargeTest; +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; + import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameters; import java.util.Arrays; import java.util.Collection; -@RunWith(Parameterized.class) +@RunWith(JUnitParamsRunner.class) @LargeTest public class StringReplacePerfTest { @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); @@ -64,8 +65,7 @@ public class StringReplacePerfTest { return stringBuilder.toString(); } - @Parameters(name = "mStringLengths={0}") - public static Collection<Object[]> data() { + public static Collection<Object[]> getData() { return Arrays.asList( new Object[][] { {StringLengths.EMPTY}, @@ -76,54 +76,57 @@ public class StringReplacePerfTest { }); } - @Parameterized.Parameter(0) - public StringLengths mStringLengths; - @Test - public void timeReplaceCharNonExistent() { + @Parameters(method = "getData") + public void timeReplaceCharNonExistent(StringLengths stringLengths) { BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { - mStringLengths.mValue.replace('z', '0'); + stringLengths.mValue.replace('z', '0'); } } @Test - public void timeReplaceCharRepeated() { + @Parameters(method = "getData") + public void timeReplaceCharRepeated(StringLengths stringLengths) { BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { - mStringLengths.mValue.replace('a', '0'); + stringLengths.mValue.replace('a', '0'); } } @Test - public void timeReplaceSingleChar() { + @Parameters(method = "getData") + public void timeReplaceSingleChar(StringLengths stringLengths) { BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { - mStringLengths.mValue.replace('q', '0'); + stringLengths.mValue.replace('q', '0'); } } @Test - public void timeReplaceSequenceNonExistent() { + @Parameters(method = "getData") + public void timeReplaceSequenceNonExistent(StringLengths stringLengths) { BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { - mStringLengths.mValue.replace("fish", "0"); + stringLengths.mValue.replace("fish", "0"); } } @Test - public void timeReplaceSequenceRepeated() { + @Parameters(method = "getData") + public void timeReplaceSequenceRepeated(StringLengths stringLengths) { BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { - mStringLengths.mValue.replace("jklm", "0"); + stringLengths.mValue.replace("jklm", "0"); } } @Test - public void timeReplaceSingleSequence() { + @Parameters(method = "getData") + public void timeReplaceSingleSequence(StringLengths stringLengths) { BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { - mStringLengths.mValue.replace("qrst", "0"); + stringLengths.mValue.replace("qrst", "0"); } } } diff --git a/apct-tests/perftests/core/src/android/libcore/regression/StringToBytesPerfTest.java b/apct-tests/perftests/core/src/android/libcore/regression/StringToBytesPerfTest.java index 1efc188f4e4d..c004d959b9b3 100644 --- a/apct-tests/perftests/core/src/android/libcore/regression/StringToBytesPerfTest.java +++ b/apct-tests/perftests/core/src/android/libcore/regression/StringToBytesPerfTest.java @@ -20,17 +20,18 @@ import android.perftests.utils.BenchmarkState; import android.perftests.utils.PerfStatusReporter; import android.test.suitebuilder.annotation.LargeTest; +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; + import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameters; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Collection; -@RunWith(Parameterized.class) +@RunWith(JUnitParamsRunner.class) @LargeTest public class StringToBytesPerfTest { @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); @@ -53,8 +54,7 @@ public class StringToBytesPerfTest { } } - @Parameters(name = "mStringLengths={0}") - public static Collection<Object[]> data() { + public static Collection<Object[]> getData() { return Arrays.asList( new Object[][] { {StringLengths.EMPTY}, @@ -69,9 +69,6 @@ public class StringToBytesPerfTest { }); } - @Parameterized.Parameter(0) - public StringLengths mStringLengths; - private static String makeString(int length) { char[] chars = new char[length]; for (int i = 0; i < length; ++i) { @@ -89,26 +86,29 @@ public class StringToBytesPerfTest { } @Test - public void timeGetBytesUtf8() { + @Parameters(method = "getData") + public void timeGetBytesUtf8(StringLengths stringLengths) { BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { - mStringLengths.mValue.getBytes(StandardCharsets.UTF_8); + stringLengths.mValue.getBytes(StandardCharsets.UTF_8); } } @Test - public void timeGetBytesIso88591() { + @Parameters(method = "getData") + public void timeGetBytesIso88591(StringLengths stringLengths) { BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { - mStringLengths.mValue.getBytes(StandardCharsets.ISO_8859_1); + stringLengths.mValue.getBytes(StandardCharsets.ISO_8859_1); } } @Test - public void timeGetBytesAscii() { + @Parameters(method = "getData") + public void timeGetBytesAscii(StringLengths stringLengths) { BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { - mStringLengths.mValue.getBytes(StandardCharsets.US_ASCII); + stringLengths.mValue.getBytes(StandardCharsets.US_ASCII); } } } diff --git a/apct-tests/perftests/core/src/android/libcore/regression/StringToRealPerfTest.java b/apct-tests/perftests/core/src/android/libcore/regression/StringToRealPerfTest.java index b01948aa2255..15516fc1c51c 100644 --- a/apct-tests/perftests/core/src/android/libcore/regression/StringToRealPerfTest.java +++ b/apct-tests/perftests/core/src/android/libcore/regression/StringToRealPerfTest.java @@ -20,22 +20,22 @@ import android.perftests.utils.BenchmarkState; import android.perftests.utils.PerfStatusReporter; import android.test.suitebuilder.annotation.LargeTest; +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; + import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameters; import java.util.Arrays; import java.util.Collection; -@RunWith(Parameterized.class) +@RunWith(JUnitParamsRunner.class) @LargeTest public class StringToRealPerfTest { @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); - @Parameters(name = "mString={0}") - public static Collection<Object[]> data() { + public static Collection<Object[]> getData() { return Arrays.asList( new Object[][] { {"NaN"}, @@ -49,22 +49,21 @@ public class StringToRealPerfTest { }); } - @Parameterized.Parameter(0) - public String mString; - @Test - public void timeFloat_parseFloat() { + @Parameters(method = "getData") + public void timeFloat_parseFloat(String string) { BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { - Float.parseFloat(mString); + Float.parseFloat(string); } } @Test - public void timeDouble_parseDouble() { + @Parameters(method = "getData") + public void timeDouble_parseDouble(String string) { BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { - Double.parseDouble(mString); + Double.parseDouble(string); } } } diff --git a/apct-tests/perftests/core/src/android/libcore/regression/XMLEntitiesPerfTest.java b/apct-tests/perftests/core/src/android/libcore/regression/XMLEntitiesPerfTest.java index 2ea834d0b71c..ae1e8bce42ac 100644 --- a/apct-tests/perftests/core/src/android/libcore/regression/XMLEntitiesPerfTest.java +++ b/apct-tests/perftests/core/src/android/libcore/regression/XMLEntitiesPerfTest.java @@ -20,12 +20,12 @@ import android.perftests.utils.BenchmarkState; import android.perftests.utils.PerfStatusReporter; import android.test.suitebuilder.annotation.LargeTest; -import org.junit.Before; +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; + import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameters; import org.xml.sax.InputSource; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserFactory; @@ -38,13 +38,12 @@ import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; // http://code.google.com/p/android/issues/detail?id=18102 -@RunWith(Parameterized.class) +@RunWith(JUnitParamsRunner.class) @LargeTest public final class XMLEntitiesPerfTest { @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); - @Parameters(name = "mLength={0}, mEntityFraction={1}") - public static Collection<Object[]> data() { + public static Collection<Object[]> getData() { return Arrays.asList( new Object[][] { {10, 0}, @@ -59,29 +58,22 @@ public final class XMLEntitiesPerfTest { }); } - @Parameterized.Parameter(0) - public int mLength; - - @Parameterized.Parameter(1) - public float mEntityFraction; - private XmlPullParserFactory mXmlPullParserFactory; private DocumentBuilderFactory mDocumentBuilderFactory; /** a string like {@code <doc>&&++</doc>}. */ private String mXml; - @Before - public void setUp() throws Exception { + public void setUp(int length, float entityFraction) throws Exception { mXmlPullParserFactory = XmlPullParserFactory.newInstance(); mDocumentBuilderFactory = DocumentBuilderFactory.newInstance(); StringBuilder xmlBuilder = new StringBuilder(); xmlBuilder.append("<doc>"); - for (int i = 0; i < (mLength * mEntityFraction); i++) { + for (int i = 0; i < (length * entityFraction); i++) { xmlBuilder.append("&"); } - while (xmlBuilder.length() < mLength) { + while (xmlBuilder.length() < length) { xmlBuilder.append("+"); } xmlBuilder.append("</doc>"); @@ -89,7 +81,9 @@ public final class XMLEntitiesPerfTest { } @Test - public void timeXmlParser() throws Exception { + @Parameters(method = "getData") + public void timeXmlParser(int length, float entityFraction) throws Exception { + setUp(length, entityFraction); BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { XmlPullParser parser = mXmlPullParserFactory.newPullParser(); @@ -101,7 +95,9 @@ public final class XMLEntitiesPerfTest { } @Test - public void timeDocumentBuilder() throws Exception { + @Parameters(method = "getData") + public void timeDocumentBuilder(int length, float entityFraction) throws Exception { + setUp(length, entityFraction); BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { DocumentBuilder documentBuilder = mDocumentBuilderFactory.newDocumentBuilder(); diff --git a/apct-tests/perftests/windowmanager/src/android/wm/WindowAddRemovePerfTest.java b/apct-tests/perftests/windowmanager/src/android/wm/WindowAddRemovePerfTest.java index f844ba3bb0a9..cc74a5294f9d 100644 --- a/apct-tests/perftests/windowmanager/src/android/wm/WindowAddRemovePerfTest.java +++ b/apct-tests/perftests/windowmanager/src/android/wm/WindowAddRemovePerfTest.java @@ -30,8 +30,8 @@ import android.view.IWindowSession; import android.view.InputChannel; import android.view.InsetsSourceControl; import android.view.InsetsState; -import android.view.InsetsVisibilities; import android.view.View; +import android.view.WindowInsets; import android.view.WindowManager; import android.view.WindowManagerGlobal; @@ -84,7 +84,7 @@ public class WindowAddRemovePerfTest extends WindowManagerPerfTestBase private static class TestWindow extends BaseIWindow { final WindowManager.LayoutParams mLayoutParams = new WindowManager.LayoutParams(); - final InsetsVisibilities mRequestedVisibilities = new InsetsVisibilities(); + final int mRequestedVisibleTypes = WindowInsets.Type.defaultVisible(); final InsetsState mOutInsetsState = new InsetsState(); final InsetsSourceControl[] mOutControls = new InsetsSourceControl[0]; final Rect mOutAttachedFrame = new Rect(); @@ -106,7 +106,7 @@ public class WindowAddRemovePerfTest extends WindowManagerPerfTestBase long startTime = SystemClock.elapsedRealtimeNanos(); session.addToDisplay(this, mLayoutParams, View.VISIBLE, - Display.DEFAULT_DISPLAY, mRequestedVisibilities, inputChannel, + Display.DEFAULT_DISPLAY, mRequestedVisibleTypes, inputChannel, mOutInsetsState, mOutControls, mOutAttachedFrame, mOutSizeCompatScale); final long elapsedTimeNsOfAdd = SystemClock.elapsedRealtimeNanos() - startTime; state.addExtraResult("add", elapsedTimeNsOfAdd); diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java index 20bca3530b63..52dc01b8df30 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java @@ -460,14 +460,14 @@ class JobConcurrencyManager { if (mPowerManager != null && mPowerManager.isDeviceIdleMode()) { synchronized (mLock) { stopUnexemptedJobsForDoze(); - stopLongRunningJobsLocked("deep doze"); + stopOvertimeJobsLocked("deep doze"); } } break; case PowerManager.ACTION_POWER_SAVE_MODE_CHANGED: if (mPowerManager != null && mPowerManager.isPowerSaveMode()) { synchronized (mLock) { - stopLongRunningJobsLocked("battery saver"); + stopOvertimeJobsLocked("battery saver"); } } break; @@ -555,7 +555,7 @@ class JobConcurrencyManager { * execution guarantee. */ @GuardedBy("mLock") - boolean isJobLongRunningLocked(@NonNull JobStatus job) { + boolean isJobInOvertimeLocked(@NonNull JobStatus job) { if (!mRunningJobs.contains(job)) { return false; } @@ -1043,7 +1043,7 @@ class JobConcurrencyManager { } @GuardedBy("mLock") - private void stopLongRunningJobsLocked(@NonNull String debugReason) { + private void stopOvertimeJobsLocked(@NonNull String debugReason) { for (int i = 0; i < mActiveServices.size(); ++i) { final JobServiceContext jsc = mActiveServices.get(i); final JobStatus jobStatus = jsc.getRunningJobLocked(); @@ -1060,7 +1060,7 @@ class JobConcurrencyManager { * restricted by the given {@link JobRestriction}. */ @GuardedBy("mLock") - void maybeStopLongRunningJobsLocked(@NonNull JobRestriction restriction) { + void maybeStopOvertimeJobsLocked(@NonNull JobRestriction restriction) { for (int i = mActiveServices.size() - 1; i >= 0; --i) { final JobServiceContext jsc = mActiveServices.get(i); final JobStatus jobStatus = jsc.getRunningJobLocked(); diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java index d28ebde9cb23..6375d0deae40 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java @@ -1870,10 +1870,10 @@ public class JobSchedulerService extends com.android.server.SystemService return mConcurrencyManager.isJobRunningLocked(job); } - /** @see JobConcurrencyManager#isJobLongRunningLocked(JobStatus) */ + /** @see JobConcurrencyManager#isJobInOvertimeLocked(JobStatus) */ @GuardedBy("mLock") - public boolean isLongRunningLocked(JobStatus job) { - return mConcurrencyManager.isJobLongRunningLocked(job); + public boolean isJobInOvertimeLocked(JobStatus job) { + return mConcurrencyManager.isJobInOvertimeLocked(job); } private void noteJobPending(JobStatus job) { @@ -2155,11 +2155,11 @@ public class JobSchedulerService extends com.android.server.SystemService @Override public void onRestrictionStateChanged(@NonNull JobRestriction restriction, - boolean stopLongRunningJobs) { + boolean stopOvertimeJobs) { mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget(); - if (stopLongRunningJobs) { + if (stopOvertimeJobs) { synchronized (mLock) { - mConcurrencyManager.maybeStopLongRunningJobsLocked(restriction); + mConcurrencyManager.maybeStopOvertimeJobsLocked(restriction); } } } diff --git a/apex/jobscheduler/service/java/com/android/server/job/StateChangedListener.java b/apex/jobscheduler/service/java/com/android/server/job/StateChangedListener.java index d7bd03025fde..554f152dccfb 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/StateChangedListener.java +++ b/apex/jobscheduler/service/java/com/android/server/job/StateChangedListener.java @@ -41,11 +41,11 @@ public interface StateChangedListener { * Called by a {@link com.android.server.job.restrictions.JobRestriction} to notify the * JobScheduler that it should check on the state of all jobs. * - * @param stopLongRunningJobs Whether to stop any jobs that have run for more than their minimum - * execution guarantee and are restricted by the changed restriction + * @param stopOvertimeJobs Whether to stop any jobs that have run for more than their minimum + * execution guarantee and are restricted by the changed restriction */ void onRestrictionStateChanged(@NonNull JobRestriction restriction, - boolean stopLongRunningJobs); + boolean stopOvertimeJobs); /** * Called by the controller to notify the JobManager that regardless of the state of the task, diff --git a/apex/jobscheduler/service/java/com/android/server/job/restrictions/ThermalStatusRestriction.java b/apex/jobscheduler/service/java/com/android/server/job/restrictions/ThermalStatusRestriction.java index a007a69a29e3..ca2fd60a07b2 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/restrictions/ThermalStatusRestriction.java +++ b/apex/jobscheduler/service/java/com/android/server/job/restrictions/ThermalStatusRestriction.java @@ -90,11 +90,11 @@ public class ThermalStatusRestriction extends JobRestriction { final int priority = job.getEffectivePriority(); if (mThermalStatus >= HIGHER_PRIORITY_THRESHOLD) { // For moderate throttling, only let expedited jobs and high priority regular jobs that - // haven't been running for long run. + // haven't been running for a long time run. return !job.shouldTreatAsExpeditedJob() && !(priority == JobInfo.PRIORITY_HIGH && mService.isCurrentlyRunningLocked(job) - && !mService.isLongRunningLocked(job)); + && !mService.isJobInOvertimeLocked(job)); } if (mThermalStatus >= LOW_PRIORITY_THRESHOLD) { // For light throttling, throttle all min priority jobs and all low priority jobs that @@ -102,7 +102,7 @@ public class ThermalStatusRestriction extends JobRestriction { return priority == JobInfo.PRIORITY_MIN || (priority == JobInfo.PRIORITY_LOW && (!mService.isCurrentlyRunningLocked(job) - || mService.isLongRunningLocked(job))); + || mService.isJobInOvertimeLocked(job))); } return false; } diff --git a/core/api/current.txt b/core/api/current.txt index ec7d6b393a4e..700725c4c59d 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -172,6 +172,7 @@ package android { field public static final String REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE = "android.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE"; field public static final String REQUEST_PASSWORD_COMPLEXITY = "android.permission.REQUEST_PASSWORD_COMPLEXITY"; field @Deprecated public static final String RESTART_PACKAGES = "android.permission.RESTART_PACKAGES"; + field public static final String RUN_LONG_JOBS = "android.permission.RUN_LONG_JOBS"; field public static final String SCHEDULE_EXACT_ALARM = "android.permission.SCHEDULE_EXACT_ALARM"; field public static final String SEND_RESPOND_VIA_MESSAGE = "android.permission.SEND_RESPOND_VIA_MESSAGE"; field public static final String SEND_SMS = "android.permission.SEND_SMS"; @@ -7456,6 +7457,7 @@ package android.app.admin { method public long getMaximumTimeToLock(@Nullable android.content.ComponentName); method @NonNull public java.util.List<java.lang.String> getMeteredDataDisabledPackages(@NonNull android.content.ComponentName); method public int getMinimumRequiredWifiSecurityLevel(); + method public int getMtePolicy(); method @RequiresPermission(value=android.Manifest.permission.READ_NEARBY_STREAMING_POLICY, conditional=true) public int getNearbyAppStreamingPolicy(); method @RequiresPermission(value=android.Manifest.permission.READ_NEARBY_STREAMING_POLICY, conditional=true) public int getNearbyNotificationStreamingPolicy(); method @Deprecated @ColorInt public int getOrganizationColor(@NonNull android.content.ComponentName); @@ -7604,6 +7606,7 @@ package android.app.admin { method public void setMaximumTimeToLock(@NonNull android.content.ComponentName, long); method @NonNull public java.util.List<java.lang.String> setMeteredDataDisabledPackages(@NonNull android.content.ComponentName, @NonNull java.util.List<java.lang.String>); method public void setMinimumRequiredWifiSecurityLevel(int); + method public void setMtePolicy(int); method public void setNearbyAppStreamingPolicy(int); method public void setNearbyNotificationStreamingPolicy(int); method public void setNetworkLoggingEnabled(@Nullable android.content.ComponentName, boolean); @@ -7787,6 +7790,9 @@ package android.app.admin { field public static final int LOCK_TASK_FEATURE_SYSTEM_INFO = 1; // 0x1 field public static final int MAKE_USER_EPHEMERAL = 2; // 0x2 field public static final String MIME_TYPE_PROVISIONING_NFC = "application/com.android.managedprovisioning"; + field public static final int MTE_DISABLED = 2; // 0x2 + field public static final int MTE_ENABLED = 1; // 0x1 + field public static final int MTE_NOT_CONTROLLED_BY_POLICY = 0; // 0x0 field public static final int NEARBY_STREAMING_DISABLED = 1; // 0x1 field public static final int NEARBY_STREAMING_ENABLED = 2; // 0x2 field public static final int NEARBY_STREAMING_NOT_CONTROLLED_BY_POLICY = 0; // 0x0 @@ -9268,6 +9274,7 @@ package android.content { field public static final int CLASSIFICATION_NOT_COMPLETE = 1; // 0x1 field public static final int CLASSIFICATION_NOT_PERFORMED = 2; // 0x2 field @NonNull public static final android.os.Parcelable.Creator<android.content.ClipDescription> CREATOR; + field public static final String EXTRA_IS_REMOTE_DEVICE = "android.content.extra.IS_REMOTE_DEVICE"; field public static final String EXTRA_IS_SENSITIVE = "android.content.extra.IS_SENSITIVE"; field public static final String MIMETYPE_TEXT_HTML = "text/html"; field public static final String MIMETYPE_TEXT_INTENT = "text/vnd.android.intent"; @@ -19496,7 +19503,7 @@ package android.location { method public boolean hasSatelliteBlocklist(); method public boolean hasSatellitePvt(); method public boolean hasScheduling(); - method public boolean hasSingleShot(); + method public boolean hasSingleShotFix(); method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.location.GnssCapabilities> CREATOR; } @@ -19528,7 +19535,7 @@ package android.location { method @NonNull public android.location.GnssCapabilities.Builder setHasSatelliteBlocklist(boolean); method @NonNull public android.location.GnssCapabilities.Builder setHasSatellitePvt(boolean); method @NonNull public android.location.GnssCapabilities.Builder setHasScheduling(boolean); - method @NonNull public android.location.GnssCapabilities.Builder setHasSingleShot(boolean); + method @NonNull public android.location.GnssCapabilities.Builder setHasSingleShotFix(boolean); } public final class GnssClock implements android.os.Parcelable { @@ -19726,7 +19733,7 @@ package android.location { method public int getConstellationType(@IntRange(from=0) int); method @FloatRange(from=0xffffffa6, to=90) public float getElevationDegrees(@IntRange(from=0) int); method @IntRange(from=0) public int getSatelliteCount(); - method @IntRange(from=1, to=200) public int getSvid(@IntRange(from=0) int); + method @IntRange(from=1, to=206) public int getSvid(@IntRange(from=0) int); method public boolean hasAlmanacData(@IntRange(from=0) int); method public boolean hasBasebandCn0DbHz(@IntRange(from=0) int); method public boolean hasCarrierFrequencyHz(@IntRange(from=0) int); @@ -44006,9 +44013,10 @@ package android.telephony { field public static final long NETWORK_TYPE_BITMASK_HSPA = 512L; // 0x200L field public static final long NETWORK_TYPE_BITMASK_HSPAP = 16384L; // 0x4000L field public static final long NETWORK_TYPE_BITMASK_HSUPA = 256L; // 0x100L + field public static final long NETWORK_TYPE_BITMASK_IDEN = 1024L; // 0x400L field public static final long NETWORK_TYPE_BITMASK_IWLAN = 131072L; // 0x20000L field public static final long NETWORK_TYPE_BITMASK_LTE = 4096L; // 0x1000L - field public static final long NETWORK_TYPE_BITMASK_LTE_CA = 262144L; // 0x40000L + field @Deprecated public static final long NETWORK_TYPE_BITMASK_LTE_CA = 262144L; // 0x40000L field public static final long NETWORK_TYPE_BITMASK_NR = 524288L; // 0x80000L field public static final long NETWORK_TYPE_BITMASK_TD_SCDMA = 65536L; // 0x10000L field public static final long NETWORK_TYPE_BITMASK_UMTS = 4L; // 0x4L @@ -47431,6 +47439,7 @@ package android.util { field public static final int DENSITY_420 = 420; // 0x1a4 field public static final int DENSITY_440 = 440; // 0x1b8 field public static final int DENSITY_450 = 450; // 0x1c2 + field public static final int DENSITY_520 = 520; // 0x208 field public static final int DENSITY_560 = 560; // 0x230 field public static final int DENSITY_600 = 600; // 0x258 field public static final int DENSITY_DEFAULT = 160; // 0xa0 diff --git a/core/api/module-lib-current.txt b/core/api/module-lib-current.txt index e890005c0479..88efcced78fb 100644 --- a/core/api/module-lib-current.txt +++ b/core/api/module-lib-current.txt @@ -301,10 +301,6 @@ package android.os { method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK}) public void reportNetworkInterfaceForTransports(@NonNull String, @NonNull int[]) throws java.lang.RuntimeException; } - public class Binder implements android.os.IBinder { - method public final void markVintfStability(); - } - public class BluetoothServiceManager { method @NonNull public android.os.BluetoothServiceManager.ServiceRegisterer getBluetoothManagerServiceRegisterer(); } diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 7a22e373045d..a382ecfc99d3 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -775,11 +775,14 @@ package android.app { } public class BroadcastOptions { + method public void clearDeliveryGroupPolicy(); method public void clearRequireCompatChange(); + method public int getDeliveryGroupPolicy(); method public boolean isPendingIntentBackgroundActivityLaunchAllowed(); method public static android.app.BroadcastOptions makeBasic(); method @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RESPONSE_STATS) public void recordResponseEventWhileInBackground(@IntRange(from=0) long); method @RequiresPermission(android.Manifest.permission.START_ACTIVITIES_FROM_BACKGROUND) public void setBackgroundActivityStartsAllowed(boolean); + method public void setDeliveryGroupPolicy(int); method public void setDontSendToRestrictedApps(boolean); method public void setPendingIntentBackgroundActivityLaunchAllowed(boolean); method public void setRequireAllOfPermissions(@Nullable String[]); @@ -788,6 +791,8 @@ package android.app { method @RequiresPermission(anyOf={android.Manifest.permission.CHANGE_DEVICE_IDLE_TEMP_WHITELIST, android.Manifest.permission.START_ACTIVITIES_FROM_BACKGROUND, android.Manifest.permission.START_FOREGROUND_SERVICES_FROM_BACKGROUND}) public void setTemporaryAppAllowlist(long, int, int, @Nullable String); method @Deprecated @RequiresPermission(anyOf={android.Manifest.permission.CHANGE_DEVICE_IDLE_TEMP_WHITELIST, android.Manifest.permission.START_ACTIVITIES_FROM_BACKGROUND, android.Manifest.permission.START_FOREGROUND_SERVICES_FROM_BACKGROUND}) public void setTemporaryAppWhitelistDuration(long); method public android.os.Bundle toBundle(); + field public static final int DELIVERY_GROUP_POLICY_ALL = 0; // 0x0 + field public static final int DELIVERY_GROUP_POLICY_MOST_RECENT = 1; // 0x1 } public class DownloadManager { @@ -9346,6 +9351,7 @@ package android.os { public class Binder implements android.os.IBinder { method public int handleShellCommand(@NonNull android.os.ParcelFileDescriptor, @NonNull android.os.ParcelFileDescriptor, @NonNull android.os.ParcelFileDescriptor, @NonNull String[]); + method public final void markVintfStability(); method public static void setProxyTransactListener(@Nullable android.os.Binder.ProxyTransactListener); } @@ -13392,7 +13398,7 @@ package android.telephony { method @NonNull @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public int[] getCompleteActiveSubscriptionIdList(); method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public int getEnabledSubscriptionId(int); method @NonNull public static android.content.res.Resources getResourcesForSubId(@NonNull android.content.Context, int); - method @Nullable @RequiresPermission(android.Manifest.permission.MANAGE_SUBSCRIPTION_USER_ASSOCIATION) public android.os.UserHandle getUserHandle(int); + method @Nullable @RequiresPermission(android.Manifest.permission.MANAGE_SUBSCRIPTION_USER_ASSOCIATION) public android.os.UserHandle getSubscriptionUserHandle(int); method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean isSubscriptionEnabled(int); method public void requestEmbeddedSubscriptionInfoListRefresh(); method public void requestEmbeddedSubscriptionInfoListRefresh(int); @@ -13402,8 +13408,8 @@ package android.telephony { method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setDefaultVoiceSubscriptionId(int); method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setPreferredDataSubscriptionId(int, boolean, @Nullable java.util.concurrent.Executor, @Nullable java.util.function.Consumer<java.lang.Integer>); method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public boolean setSubscriptionEnabled(int, boolean); + method @RequiresPermission(android.Manifest.permission.MANAGE_SUBSCRIPTION_USER_ASSOCIATION) public void setSubscriptionUserHandle(int, @Nullable android.os.UserHandle); method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setUiccApplicationsEnabled(int, boolean); - method @RequiresPermission(android.Manifest.permission.MANAGE_SUBSCRIPTION_USER_ASSOCIATION) public void setUserHandle(int, @Nullable android.os.UserHandle); field @RequiresPermission(android.Manifest.permission.MANAGE_SUBSCRIPTION_PLANS) public static final String ACTION_SUBSCRIPTION_PLANS_CHANGED = "android.telephony.action.SUBSCRIPTION_PLANS_CHANGED"; field @NonNull public static final android.net.Uri ADVANCED_CALLING_ENABLED_CONTENT_URI; field @NonNull public static final android.net.Uri CROSS_SIM_ENABLED_CONTENT_URI; diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java index 4b1b0a260e27..308e996f9919 100644 --- a/core/java/android/app/ActivityManagerInternal.java +++ b/core/java/android/app/ActivityManagerInternal.java @@ -389,6 +389,10 @@ public abstract class ActivityManagerInternal { */ public abstract boolean isAppStartModeDisabled(int uid, String packageName); + /** + * Returns the ids of the current user and all of its profiles (if any), regardless of the + * running state of the profiles. + */ public abstract int[] getCurrentProfileIds(); public abstract UserInfo getCurrentUser(); public abstract void ensureNotSpecialUser(@UserIdInt int userId); diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java index d1275f66383e..1b972e0cb81a 100644 --- a/core/java/android/app/AppOpsManager.java +++ b/core/java/android/app/AppOpsManager.java @@ -1353,9 +1353,16 @@ public class AppOpsManager { public static final int OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO = AppProtoEnums.APP_OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO; + /** + * App can schedule long running jobs. + * + * @hide + */ + public static final int OP_RUN_LONG_JOBS = AppProtoEnums.APP_OP_RUN_LONG_JOBS; + /** @hide */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) - public static final int _NUM_OP = 122; + public static final int _NUM_OP = 123; /** Access to coarse location information. */ public static final String OPSTR_COARSE_LOCATION = "android:coarse_location"; @@ -1839,6 +1846,13 @@ public class AppOpsManager { public static final String OPSTR_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO = "android:receive_explicit_user_interaction_audio"; + /** + * App can schedule long running jobs. + * + * @hide + */ + public static final String OPSTR_RUN_LONG_JOBS = "android:run_long_jobs"; + /** {@link #sAppOpsToNote} not initialized yet for this op */ private static final byte SHOULD_COLLECT_NOTE_OP_NOT_INITIALIZED = 0; /** Should not collect noting of this app-op in {@link #sAppOpsToNote} */ @@ -1933,6 +1947,7 @@ public class AppOpsManager { OP_SCHEDULE_EXACT_ALARM, OP_MANAGE_MEDIA, OP_TURN_SCREEN_ON, + OP_RUN_LONG_JOBS, }; static final AppOpInfo[] sAppOpInfos = new AppOpInfo[]{ @@ -2312,7 +2327,9 @@ public class AppOpsManager { new AppOpInfo.Builder(OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO, OPSTR_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO, "RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO").setDefaultMode( - AppOpsManager.MODE_ALLOWED).build() + AppOpsManager.MODE_ALLOWED).build(), + new AppOpInfo.Builder(OP_RUN_LONG_JOBS, OPSTR_RUN_LONG_JOBS, "RUN_LONG_JOBS") + .setPermission(Manifest.permission.RUN_LONG_JOBS).build() }; /** diff --git a/core/java/android/app/BroadcastOptions.java b/core/java/android/app/BroadcastOptions.java index cc4650a7df71..48638d1fdff4 100644 --- a/core/java/android/app/BroadcastOptions.java +++ b/core/java/android/app/BroadcastOptions.java @@ -31,6 +31,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.os.Build; import android.os.Bundle; +import android.os.BundleMerger; import android.os.PowerExemptionManager; import android.os.PowerExemptionManager.ReasonCode; import android.os.PowerExemptionManager.TempAllowListType; @@ -67,6 +68,7 @@ public class BroadcastOptions extends ComponentOptions { private @Nullable IntentFilter mRemoveMatchingFilter; private @DeliveryGroupPolicy int mDeliveryGroupPolicy; private @Nullable String mDeliveryGroupKey; + private @Nullable BundleMerger mDeliveryGroupExtrasMerger; /** * Change ID which is invalid. @@ -218,6 +220,12 @@ public class BroadcastOptions extends ComponentOptions { "android:broadcast.deliveryGroupKey"; /** + * Corresponds to {@link #setDeliveryGroupExtrasMerger(BundleMerger)}. + */ + private static final String KEY_DELIVERY_GROUP_EXTRAS_MERGER = + "android:broadcast.deliveryGroupExtrasMerger"; + + /** * The list of delivery group policies which specify how multiple broadcasts belonging to * the same delivery group has to be handled. * @hide @@ -225,6 +233,7 @@ public class BroadcastOptions extends ComponentOptions { @IntDef(flag = true, prefix = { "DELIVERY_GROUP_POLICY_" }, value = { DELIVERY_GROUP_POLICY_ALL, DELIVERY_GROUP_POLICY_MOST_RECENT, + DELIVERY_GROUP_POLICY_MERGED, }) @Retention(RetentionPolicy.SOURCE) public @interface DeliveryGroupPolicy {} @@ -235,6 +244,7 @@ public class BroadcastOptions extends ComponentOptions { * * @hide */ + @SystemApi public static final int DELIVERY_GROUP_POLICY_ALL = 0; /** @@ -243,8 +253,17 @@ public class BroadcastOptions extends ComponentOptions { * * @hide */ + @SystemApi public static final int DELIVERY_GROUP_POLICY_MOST_RECENT = 1; + /** + * Delivery group policy that indicates that the extras data from the broadcasts in the + * delivery group need to be merged into a single broadcast and the rest can be dropped. + * + * @hide + */ + public static final int DELIVERY_GROUP_POLICY_MERGED = 2; + public static BroadcastOptions makeBasic() { BroadcastOptions opts = new BroadcastOptions(); return opts; @@ -295,6 +314,8 @@ public class BroadcastOptions extends ComponentOptions { mDeliveryGroupPolicy = opts.getInt(KEY_DELIVERY_GROUP_POLICY, DELIVERY_GROUP_POLICY_ALL); mDeliveryGroupKey = opts.getString(KEY_DELIVERY_GROUP_KEY); + mDeliveryGroupExtrasMerger = opts.getParcelable(KEY_DELIVERY_GROUP_EXTRAS_MERGER, + BundleMerger.class); } /** @@ -724,16 +745,35 @@ public class BroadcastOptions extends ComponentOptions { * * @hide */ + @SystemApi public void setDeliveryGroupPolicy(@DeliveryGroupPolicy int policy) { mDeliveryGroupPolicy = policy; } - /** @hide */ + /** + * Get the delivery group policy for this broadcast that specifies how multiple broadcasts + * belonging to the same delivery group has to be handled. + * + * @hide + */ + @SystemApi public @DeliveryGroupPolicy int getDeliveryGroupPolicy() { return mDeliveryGroupPolicy; } /** + * Clears any previously set delivery group policies using + * {@link #setDeliveryGroupKey(String, String)} and resets the delivery group policy to + * the default value ({@link #DELIVERY_GROUP_POLICY_ALL}). + * + * @hide + */ + @SystemApi + public void clearDeliveryGroupPolicy() { + mDeliveryGroupPolicy = DELIVERY_GROUP_POLICY_ALL; + } + + /** * Set namespace and key to identify the delivery group that this broadcast belongs to. * If no namespace and key is set, then by default {@link Intent#filterEquals(Intent)} will be * used to identify the delivery group. @@ -754,12 +794,35 @@ public class BroadcastOptions extends ComponentOptions { } /** + * Set the {@link BundleMerger} that specifies how to merge the extras data from + * broadcasts in a delivery group. + * + * <p>Note that this value will be ignored if the delivery group policy is not set as + * {@link #DELIVERY_GROUP_POLICY_MERGED}. + * + * @hide + */ + public void setDeliveryGroupExtrasMerger(@NonNull BundleMerger extrasMerger) { + Preconditions.checkNotNull(extrasMerger); + mDeliveryGroupExtrasMerger = extrasMerger; + } + + /** @hide */ + public @Nullable BundleMerger getDeliveryGroupExtrasMerger() { + return mDeliveryGroupExtrasMerger; + } + + /** * Returns the created options as a Bundle, which can be passed to * {@link android.content.Context#sendBroadcast(android.content.Intent) * Context.sendBroadcast(Intent)} and related methods. * Note that the returned Bundle is still owned by the BroadcastOptions * object; you must not modify it, but can supply it to the sendBroadcast * methods that take an options Bundle. + * + * @throws IllegalStateException if the broadcast option values are inconsistent. For example, + * if the delivery group policy is specified as "MERGED" but no + * extras merger is supplied. */ @Override public Bundle toBundle() { @@ -810,6 +873,15 @@ public class BroadcastOptions extends ComponentOptions { if (mDeliveryGroupKey != null) { b.putString(KEY_DELIVERY_GROUP_KEY, mDeliveryGroupKey); } + if (mDeliveryGroupPolicy == DELIVERY_GROUP_POLICY_MERGED) { + if (mDeliveryGroupExtrasMerger != null) { + b.putParcelable(KEY_DELIVERY_GROUP_EXTRAS_MERGER, + mDeliveryGroupExtrasMerger); + } else { + throw new IllegalStateException("Extras merger cannot be empty " + + "when delivery group policy is 'MERGED'"); + } + } return b.isEmpty() ? null : b; } } diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java index 08a6b8c4e135..aaa3d21a0b25 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -173,6 +173,7 @@ import android.os.IThermalService; import android.os.IUserManager; import android.os.IncidentManager; import android.os.PerformanceHintManager; +import android.os.PermissionEnforcer; import android.os.PowerManager; import android.os.RecoverySystem; import android.os.ServiceManager; @@ -1366,6 +1367,14 @@ public final class SystemServiceRegistry { return new PermissionCheckerManager(ctx.getOuterContext()); }}); + registerService(Context.PERMISSION_ENFORCER_SERVICE, PermissionEnforcer.class, + new CachedServiceFetcher<PermissionEnforcer>() { + @Override + public PermissionEnforcer createService(ContextImpl ctx) + throws ServiceNotFoundException { + return new PermissionEnforcer(ctx.getOuterContext()); + }}); + registerService(Context.DYNAMIC_SYSTEM_SERVICE, DynamicSystemManager.class, new CachedServiceFetcher<DynamicSystemManager>() { @Override diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index de196870ef4e..f4cee5a09ff4 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -3864,6 +3864,56 @@ public class DevicePolicyManager { public static final String EXTRA_RESOURCE_IDS = "android.app.extra.RESOURCE_IDS"; + /** Allow the user to choose whether to enable MTE on the device. */ + public static final int MTE_NOT_CONTROLLED_BY_POLICY = 0; + + /** + * Require that MTE be enabled on the device, if supported. Can be set by a device owner or a + * profile owner of an organization-owned managed profile. + */ + public static final int MTE_ENABLED = 1; + + /** Require that MTE be disabled on the device. Can be set by a device owner. */ + public static final int MTE_DISABLED = 2; + + /** @hide */ + @IntDef( + prefix = {"MTE_"}, + value = {MTE_ENABLED, MTE_DISABLED, MTE_NOT_CONTROLLED_BY_POLICY}) + @Retention(RetentionPolicy.SOURCE) + public static @interface MtePolicy {} + + /** + * Set MTE policy for device. MTE_ENABLED does not necessarily enable MTE if set on a device + * that does not support MTE. + * + * The default policy is MTE_NOT_CONTROLLED_BY_POLICY. + * + * Memory Tagging Extension (MTE) is a CPU extension that allows to protect against certain + * classes of security problems at a small runtime performance cost overhead. + * + * @param policy the policy to be set + */ + public void setMtePolicy(@MtePolicy int policy) { + // TODO(b/244290023): implement + // This is SecurityException to temporarily make ParentProfileTest happy. + // This is not used. + throw new SecurityException("not implemented"); + } + + /** + * Get currently set MTE policy. This is not necessarily the same as the state of MTE on the + * device, as the device might not support MTE. + * + * @return the currently set policy + */ + public @MtePolicy int getMtePolicy() { + // TODO(b/244290023): implement + // This is SecurityException to temporarily make ParentProfileTest happy. + // This is not used. + throw new SecurityException("not implemented"); + } + /** * This object is a single place to tack on invalidation and disable calls. All * binder caches in this class derive from this Config, so all can be invalidated or diff --git a/core/java/android/app/backup/BackupRestoreEventLogger.java b/core/java/android/app/backup/BackupRestoreEventLogger.java index b789b38c966e..760c6f0fc333 100644 --- a/core/java/android/app/backup/BackupRestoreEventLogger.java +++ b/core/java/android/app/backup/BackupRestoreEventLogger.java @@ -19,10 +19,15 @@ package android.app.backup; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.util.Slog; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import java.util.Collections; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -38,6 +43,8 @@ import java.util.Map; * @hide */ public class BackupRestoreEventLogger { + private static final String TAG = "BackupRestoreEventLogger"; + /** * Max number of unique data types for which an instance of this logger can store info. Attempts * to use more distinct data type values will be rejected. @@ -72,6 +79,8 @@ public class BackupRestoreEventLogger { public @interface BackupRestoreError {} private final int mOperationType; + private final Map<String, DataTypeResult> mResults = new HashMap<>(); + private final MessageDigest mHashDigest; /** * @param operationType type of the operation for which logging will be performed. See @@ -81,6 +90,14 @@ public class BackupRestoreEventLogger { */ public BackupRestoreEventLogger(@OperationType int operationType) { mOperationType = operationType; + + MessageDigest hashDigest = null; + try { + hashDigest = MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + Slog.w("Couldn't create MessageDigest for hash computation", e); + } + mHashDigest = hashDigest; } /** @@ -98,7 +115,7 @@ public class BackupRestoreEventLogger { * @return boolean, indicating whether the log has been accepted. */ public boolean logItemsBackedUp(@NonNull @BackupRestoreDataType String dataType, int count) { - return true; + return logSuccess(OperationType.BACKUP, dataType, count); } /** @@ -118,7 +135,7 @@ public class BackupRestoreEventLogger { */ public boolean logItemsBackupFailed(@NonNull @BackupRestoreDataType String dataType, int count, @Nullable @BackupRestoreError String error) { - return true; + return logFailure(OperationType.BACKUP, dataType, count, error); } /** @@ -139,7 +156,7 @@ public class BackupRestoreEventLogger { */ public boolean logBackupMetaData(@NonNull @BackupRestoreDataType String dataType, @NonNull String metaData) { - return true; + return logMetaData(OperationType.BACKUP, dataType, metaData); } /** @@ -159,7 +176,7 @@ public class BackupRestoreEventLogger { * @return boolean, indicating whether the log has been accepted. */ public boolean logItemsRestored(@NonNull @BackupRestoreDataType String dataType, int count) { - return true; + return logSuccess(OperationType.RESTORE, dataType, count); } /** @@ -181,7 +198,7 @@ public class BackupRestoreEventLogger { */ public boolean logItemsRestoreFailed(@NonNull @BackupRestoreDataType String dataType, int count, @Nullable @BackupRestoreError String error) { - return true; + return logFailure(OperationType.RESTORE, dataType, count, error); } /** @@ -204,7 +221,7 @@ public class BackupRestoreEventLogger { */ public boolean logRestoreMetadata(@NonNull @BackupRestoreDataType String dataType, @NonNull String metadata) { - return true; + return logMetaData(OperationType.RESTORE, dataType, metadata); } /** @@ -214,7 +231,7 @@ public class BackupRestoreEventLogger { * @hide */ public List<DataTypeResult> getLoggingResults() { - return Collections.emptyList(); + return new ArrayList<>(mResults.values()); } /** @@ -227,22 +244,97 @@ public class BackupRestoreEventLogger { return mOperationType; } + private boolean logSuccess(@OperationType int operationType, + @BackupRestoreDataType String dataType, int count) { + DataTypeResult dataTypeResult = getDataTypeResult(operationType, dataType); + if (dataTypeResult == null) { + return false; + } + + dataTypeResult.mSuccessCount += count; + mResults.put(dataType, dataTypeResult); + + return true; + } + + private boolean logFailure(@OperationType int operationType, + @NonNull @BackupRestoreDataType String dataType, int count, + @Nullable @BackupRestoreError String error) { + DataTypeResult dataTypeResult = getDataTypeResult(operationType, dataType); + if (dataTypeResult == null) { + return false; + } + + dataTypeResult.mFailCount += count; + if (error != null) { + dataTypeResult.mErrors.merge(error, count, Integer::sum); + } + + return true; + } + + private boolean logMetaData(@OperationType int operationType, + @NonNull @BackupRestoreDataType String dataType, @NonNull String metaData) { + if (mHashDigest == null) { + return false; + } + DataTypeResult dataTypeResult = getDataTypeResult(operationType, dataType); + if (dataTypeResult == null) { + return false; + } + + dataTypeResult.mMetadataHash = getMetaDataHash(metaData); + + return true; + } + + /** + * Get the result container for the given data type. + * + * @return {@code DataTypeResult} object corresponding to the given {@code dataType} or + * {@code null} if the logger can't accept logs for the given data type. + */ + @Nullable + private DataTypeResult getDataTypeResult(@OperationType int operationType, + @BackupRestoreDataType String dataType) { + if (operationType != mOperationType) { + // Operation type for which we're trying to record logs doesn't match the operation + // type for which this logger instance was created. + Slog.d(TAG, "Operation type mismatch: logger created for " + mOperationType + + ", trying to log for " + operationType); + return null; + } + + if (!mResults.containsKey(dataType)) { + if (mResults.keySet().size() == DATA_TYPES_ALLOWED) { + // This is a new data type and we're already at capacity. + Slog.d(TAG, "Logger is full, ignoring new data type"); + return null; + } + + mResults.put(dataType, new DataTypeResult(dataType)); + } + + return mResults.get(dataType); + } + + private byte[] getMetaDataHash(String metaData) { + return mHashDigest.digest(metaData.getBytes(StandardCharsets.UTF_8)); + } + /** * Encapsulate logging results for a single data type. */ public static class DataTypeResult { @BackupRestoreDataType private final String mDataType; - private final int mSuccessCount; - private final Map<String, Integer> mErrors; - private final byte[] mMetadataHash; + private int mSuccessCount; + private int mFailCount; + private final Map<String, Integer> mErrors = new HashMap<>(); + private byte[] mMetadataHash; - public DataTypeResult(String dataType, int successCount, - Map<String, Integer> errors, byte[] metadataHash) { + public DataTypeResult(String dataType) { mDataType = dataType; - mSuccessCount = successCount; - mErrors = errors; - mMetadataHash = metadataHash; } @NonNull @@ -260,6 +352,13 @@ public class BackupRestoreEventLogger { } /** + * @return number of items of the given data type that have failed to back up or restore. + */ + public int getFailCount() { + return mFailCount; + } + + /** * @return mapping of {@link BackupRestoreError} to the count of items that are affected by * the error. */ diff --git a/core/java/android/content/ClipDescription.java b/core/java/android/content/ClipDescription.java index bf466116009b..de2ba44ca393 100644 --- a/core/java/android/content/ClipDescription.java +++ b/core/java/android/content/ClipDescription.java @@ -139,21 +139,28 @@ public class ClipDescription implements Parcelable { * password or credit card number. * <p> * Type: boolean - * </p> * <p> * This extra can be used to indicate that a ClipData contains sensitive information that * should be redacted or hidden from view until a user takes explicit action to reveal it * (e.g., by pasting). - * </p> * <p> * Adding this extra does not change clipboard behavior or add additional security to * the ClipData. Its purpose is essentially a rendering hint from the source application, * asking that the data within be obfuscated or redacted, unless the user has taken action * to make it visible. - * </p> */ public static final String EXTRA_IS_SENSITIVE = "android.content.extra.IS_SENSITIVE"; + /** Indicates that a ClipData's source is a remote device. + * <p> + * Type: boolean + * <p> + * This extra can be used to indicate that a ClipData comes from a separate device rather + * than being local. It is a rendering hint that can be used to take different behavior + * based on the source device of copied data. + */ + public static final String EXTRA_IS_REMOTE_DEVICE = "android.content.extra.IS_REMOTE_DEVICE"; + /** @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef(value = diff --git a/core/java/android/content/ComponentName.java b/core/java/android/content/ComponentName.java index 5f859846a5c1..f12e971afb1f 100644 --- a/core/java/android/content/ComponentName.java +++ b/core/java/android/content/ComponentName.java @@ -314,17 +314,14 @@ public final class ComponentName implements Parcelable, Cloneable, Comparable<Co */ @Override public boolean equals(@Nullable Object obj) { - try { - if (obj != null) { - ComponentName other = (ComponentName)obj; - // Note: no null checks, because mPackage and mClass can - // never be null. - return mPackage.equals(other.mPackage) - && mClass.equals(other.mClass); - } - } catch (ClassCastException e) { + if (obj instanceof ComponentName) { + ComponentName other = (ComponentName) obj; + // mPackage and mClass can never be null. + return mPackage.equals(other.mPackage) + && mClass.equals(other.mClass); + } else { + return false; } - return false; } @Override diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index d65210b8a0bc..1df0fa8084a5 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -5142,6 +5142,14 @@ public abstract class Context { public static final String PERMISSION_CHECKER_SERVICE = "permission_checker"; /** + * Official published name of the (internal) permission enforcer service. + * + * @see #getSystemService(String) + * @hide + */ + public static final String PERMISSION_ENFORCER_SERVICE = "permission_enforcer"; + + /** * Use with {@link #getSystemService(String) to retrieve an * {@link android.apphibernation.AppHibernationManager}} for * communicating with the hibernation service. @@ -5194,6 +5202,15 @@ public abstract class Context { public static final String DROPBOX_SERVICE = "dropbox"; /** + * System service name for BackgroundInstallControlService. This service supervises the MBAs + * on device and provides the related metadata of the MBAs. + * + * @hide + */ + @SuppressLint("ServiceName") + public static final String BACKGROUND_INSTALL_CONTROL_SERVICE = "background_install_control"; + + /** * System service name for BinaryTransparencyService. This is used to retrieve measurements * pertaining to various pre-installed and system binaries on device for the purposes of * providing transparency to the user. diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index 43fa61782bf6..f2ebec6306f8 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -49,6 +49,7 @@ import android.graphics.Rect; import android.net.Uri; import android.os.Build; import android.os.Bundle; +import android.os.BundleMerger; import android.os.IBinder; import android.os.IncidentManager; import android.os.Parcel; @@ -11072,6 +11073,20 @@ public class Intent implements Parcelable, Cloneable { } /** + * Merge the extras data in this intent with that of other supplied intent using the + * strategy specified using {@code extrasMerger}. + * + * <p> Note the extras data in this intent is treated as the {@code first} param + * and the extras data in {@code other} intent is treated as the {@code last} param + * when using the passed in {@link BundleMerger} object. + * + * @hide + */ + public void mergeExtras(@NonNull Intent other, @NonNull BundleMerger extrasMerger) { + mExtras = extrasMerger.merge(mExtras, other.mExtras); + } + + /** * Wrapper class holding an Intent and implementing comparisons on it for * the purpose of filtering. The class implements its * {@link #equals equals()} and {@link #hashCode hashCode()} methods as diff --git a/core/jni/include_vm/android_runtime/vm.h b/core/java/android/content/pm/IBackgroundInstallControlService.aidl index a6e7c162d6ed..c8e7caebc821 100644 --- a/core/jni/include_vm/android_runtime/vm.h +++ b/core/java/android/content/pm/IBackgroundInstallControlService.aidl @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 The Android Open Source Project + * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,11 +14,13 @@ * limitations under the License. */ -#pragma once +package android.content.pm; -#include <jni.h> +import android.content.pm.ParceledListSlice; -// Get the Java VM. If the symbol doesn't exist at runtime, it means libandroid_runtime -// is not loaded in the current process. If the symbol exists but it returns nullptr, it -// means JavaVM is not yet started. -extern "C" JavaVM* AndroidRuntimeGetJavaVM(); +/** + * {@hide} + */ +interface IBackgroundInstallControlService { + ParceledListSlice getBackgroundInstalledPackages(long flags, int userId); +} diff --git a/core/java/android/credentials/CredentialManager.java b/core/java/android/credentials/CredentialManager.java index b9cef0fccddf..30ee118d3b59 100644 --- a/core/java/android/credentials/CredentialManager.java +++ b/core/java/android/credentials/CredentialManager.java @@ -85,7 +85,7 @@ public final class CredentialManager { ICancellationSignal cancelRemote = null; try { cancelRemote = mService.executeGetCredential(request, - new GetCredentialTransport(executor, callback)); + new GetCredentialTransport(executor, callback), mContext.getOpPackageName()); } catch (RemoteException e) { e.rethrowFromSystemServer(); } @@ -124,7 +124,8 @@ public final class CredentialManager { ICancellationSignal cancelRemote = null; try { cancelRemote = mService.executeCreateCredential(request, - new CreateCredentialTransport(executor, callback)); + new CreateCredentialTransport(executor, callback), + mContext.getOpPackageName()); } catch (RemoteException e) { e.rethrowFromSystemServer(); } diff --git a/core/java/android/credentials/ICredentialManager.aidl b/core/java/android/credentials/ICredentialManager.aidl index dcf7106d15eb..b0f27f9164f3 100644 --- a/core/java/android/credentials/ICredentialManager.aidl +++ b/core/java/android/credentials/ICredentialManager.aidl @@ -29,7 +29,7 @@ import android.os.ICancellationSignal; */ interface ICredentialManager { - @nullable ICancellationSignal executeGetCredential(in GetCredentialRequest request, in IGetCredentialCallback callback); + @nullable ICancellationSignal executeGetCredential(in GetCredentialRequest request, in IGetCredentialCallback callback, String callingPackage); - @nullable ICancellationSignal executeCreateCredential(in CreateCredentialRequest request, in ICreateCredentialCallback callback); + @nullable ICancellationSignal executeCreateCredential(in CreateCredentialRequest request, in ICreateCredentialCallback callback, String callingPackage); } diff --git a/core/java/android/credentials/ui/ProviderData.java b/core/java/android/credentials/ui/ProviderData.java index 35e12fa43b28..3728469d723d 100644 --- a/core/java/android/credentials/ui/ProviderData.java +++ b/core/java/android/credentials/ui/ProviderData.java @@ -43,10 +43,10 @@ public class ProviderData implements Parcelable { "android.credentials.ui.extra.PROVIDER_DATA_LIST"; @NonNull - private final String mProviderId; + private final String mProviderFlattenedComponentName; @NonNull private final String mProviderDisplayName; - @NonNull + @Nullable private final Icon mIcon; @NonNull private final List<Entry> mCredentialEntries; @@ -58,11 +58,11 @@ public class ProviderData implements Parcelable { private final @CurrentTimeMillisLong long mLastUsedTimeMillis; public ProviderData( - @NonNull String providerId, @NonNull String providerDisplayName, - @NonNull Icon icon, @NonNull List<Entry> credentialEntries, + @NonNull String providerFlattenedComponentName, @NonNull String providerDisplayName, + @Nullable Icon icon, @NonNull List<Entry> credentialEntries, @NonNull List<Entry> actionChips, @Nullable Entry authenticationEntry, @CurrentTimeMillisLong long lastUsedTimeMillis) { - mProviderId = providerId; + mProviderFlattenedComponentName = providerFlattenedComponentName; mProviderDisplayName = providerDisplayName; mIcon = icon; mCredentialEntries = credentialEntries; @@ -73,8 +73,8 @@ public class ProviderData implements Parcelable { /** Returns the unique provider id. */ @NonNull - public String getProviderId() { - return mProviderId; + public String getProviderFlattenedComponentName() { + return mProviderFlattenedComponentName; } @NonNull @@ -82,7 +82,7 @@ public class ProviderData implements Parcelable { return mProviderDisplayName; } - @NonNull + @Nullable public Icon getIcon() { return mIcon; } @@ -108,9 +108,9 @@ public class ProviderData implements Parcelable { } protected ProviderData(@NonNull Parcel in) { - String providerId = in.readString8(); - mProviderId = providerId; - AnnotationValidations.validate(NonNull.class, null, mProviderId); + String providerFlattenedComponentName = in.readString8(); + mProviderFlattenedComponentName = providerFlattenedComponentName; + AnnotationValidations.validate(NonNull.class, null, mProviderFlattenedComponentName); String providerDisplayName = in.readString8(); mProviderDisplayName = providerDisplayName; @@ -118,7 +118,6 @@ public class ProviderData implements Parcelable { Icon icon = in.readTypedObject(Icon.CREATOR); mIcon = icon; - AnnotationValidations.validate(NonNull.class, null, mIcon); List<Entry> credentialEntries = new ArrayList<>(); in.readTypedList(credentialEntries, Entry.CREATOR); @@ -139,7 +138,7 @@ public class ProviderData implements Parcelable { @Override public void writeToParcel(@NonNull Parcel dest, int flags) { - dest.writeString8(mProviderId); + dest.writeString8(mProviderFlattenedComponentName); dest.writeString8(mProviderDisplayName); dest.writeTypedObject(mIcon, flags); dest.writeTypedList(mCredentialEntries); @@ -171,26 +170,27 @@ public class ProviderData implements Parcelable { * @hide */ public static class Builder { - private @NonNull String mProviderId; + private @NonNull String mProviderFlattenedComponentName; private @NonNull String mProviderDisplayName; - private @NonNull Icon mIcon; + private @Nullable Icon mIcon; private @NonNull List<Entry> mCredentialEntries = new ArrayList<>(); private @NonNull List<Entry> mActionChips = new ArrayList<>(); private @Nullable Entry mAuthenticationEntry = null; private @CurrentTimeMillisLong long mLastUsedTimeMillis = 0L; /** Constructor with required properties. */ - public Builder(@NonNull String providerId, @NonNull String providerDisplayName, - @NonNull Icon icon) { - mProviderId = providerId; + public Builder(@NonNull String providerFlattenedComponentName, + @NonNull String providerDisplayName, + @Nullable Icon icon) { + mProviderFlattenedComponentName = providerFlattenedComponentName; mProviderDisplayName = providerDisplayName; mIcon = icon; } /** Sets the unique provider id. */ @NonNull - public Builder setProviderId(@NonNull String providerId) { - mProviderId = providerId; + public Builder setProviderFlattenedComponentName(@NonNull String providerFlattenedComponentName) { + mProviderFlattenedComponentName = providerFlattenedComponentName; return this; } @@ -239,7 +239,8 @@ public class ProviderData implements Parcelable { /** Builds a {@link ProviderData}. */ @NonNull public ProviderData build() { - return new ProviderData(mProviderId, mProviderDisplayName, mIcon, mCredentialEntries, + return new ProviderData(mProviderFlattenedComponentName, mProviderDisplayName, + mIcon, mCredentialEntries, mActionChips, mAuthenticationEntry, mLastUsedTimeMillis); } } diff --git a/core/java/android/hardware/camera2/extension/CameraSessionConfig.aidl b/core/java/android/hardware/camera2/extension/CameraSessionConfig.aidl index 97ce183ac191..84ca2b63fbcf 100644 --- a/core/java/android/hardware/camera2/extension/CameraSessionConfig.aidl +++ b/core/java/android/hardware/camera2/extension/CameraSessionConfig.aidl @@ -24,4 +24,5 @@ parcelable CameraSessionConfig List<CameraOutputConfig> outputConfigs; CameraMetadataNative sessionParameter; int sessionTemplateId; + int sessionType; } diff --git a/core/java/android/hardware/camera2/extension/IImageCaptureExtenderImpl.aidl b/core/java/android/hardware/camera2/extension/IImageCaptureExtenderImpl.aidl index a8a7866e5ca4..3a0c3a5487e2 100644 --- a/core/java/android/hardware/camera2/extension/IImageCaptureExtenderImpl.aidl +++ b/core/java/android/hardware/camera2/extension/IImageCaptureExtenderImpl.aidl @@ -31,6 +31,7 @@ interface IImageCaptureExtenderImpl @nullable CaptureStageImpl onPresetSession(); @nullable CaptureStageImpl onEnableSession(); @nullable CaptureStageImpl onDisableSession(); + int getSessionType(); boolean isExtensionAvailable(in String cameraId, in CameraMetadataNative chars); void init(in String cameraId, in CameraMetadataNative chars); diff --git a/core/java/android/hardware/camera2/extension/IPreviewExtenderImpl.aidl b/core/java/android/hardware/camera2/extension/IPreviewExtenderImpl.aidl index 2d673448fe27..01046d01233c 100644 --- a/core/java/android/hardware/camera2/extension/IPreviewExtenderImpl.aidl +++ b/core/java/android/hardware/camera2/extension/IPreviewExtenderImpl.aidl @@ -34,6 +34,7 @@ interface IPreviewExtenderImpl void init(in String cameraId, in CameraMetadataNative chars); boolean isExtensionAvailable(in String cameraId, in CameraMetadataNative chars); @nullable CaptureStageImpl getCaptureStage(); + int getSessionType(); const int PROCESSOR_TYPE_REQUEST_UPDATE_ONLY = 0; const int PROCESSOR_TYPE_IMAGE_PROCESSOR = 1; diff --git a/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java index c8dc2d0b0b91..01c1ef4136e6 100644 --- a/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java +++ b/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java @@ -243,9 +243,16 @@ public final class CameraAdvancedExtensionSessionImpl extends CameraExtensionSes mCameraConfigMap.put(cameraOutput.getSurface(), output); } - SessionConfiguration sessionConfiguration = new SessionConfiguration( - SessionConfiguration.SESSION_REGULAR, outputList, - new CameraExtensionUtils.HandlerExecutor(mHandler), new SessionStateHandler()); + int sessionType = SessionConfiguration.SESSION_REGULAR; + if (sessionConfig.sessionType != -1 && + (sessionConfig.sessionType != SessionConfiguration.SESSION_HIGH_SPEED)) { + sessionType = sessionConfig.sessionType; + Log.v(TAG, "Using session type: " + sessionType); + } + + SessionConfiguration sessionConfiguration = new SessionConfiguration(sessionType, + outputList, new CameraExtensionUtils.HandlerExecutor(mHandler), + new SessionStateHandler()); if ((sessionConfig.sessionParameter != null) && (!sessionConfig.sessionParameter.isEmpty())) { diff --git a/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java index 41822e77f953..1f9f3b88e41d 100644 --- a/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java +++ b/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java @@ -415,6 +415,18 @@ public final class CameraExtensionSessionImpl extends CameraExtensionSession { "Session already initialized"); return; } + int previewSessionType = mPreviewExtender.getSessionType(); + int imageSessionType = mImageExtender.getSessionType(); + if (previewSessionType != imageSessionType) { + throw new IllegalStateException("Preview extender session type: " + previewSessionType + + "and image extender session type: " + imageSessionType + " mismatch!"); + } + int sessionType = SessionConfiguration.SESSION_REGULAR; + if ((previewSessionType != -1) && + (previewSessionType != SessionConfiguration.SESSION_HIGH_SPEED)) { + sessionType = previewSessionType; + Log.v(TAG, "Using session type: " + sessionType); + } ArrayList<CaptureStageImpl> sessionParamsList = new ArrayList<>(); ArrayList<OutputConfiguration> outputList = new ArrayList<>(); @@ -432,7 +444,7 @@ public final class CameraExtensionSessionImpl extends CameraExtensionSession { } SessionConfiguration sessionConfig = new SessionConfiguration( - SessionConfiguration.SESSION_REGULAR, + sessionType, outputList, new CameraExtensionUtils.HandlerExecutor(mHandler), new SessionStateHandler()); diff --git a/core/java/android/hardware/fingerprint/FingerprintManager.java b/core/java/android/hardware/fingerprint/FingerprintManager.java index 5403f089b308..3c73eb697a69 100644 --- a/core/java/android/hardware/fingerprint/FingerprintManager.java +++ b/core/java/android/hardware/fingerprint/FingerprintManager.java @@ -918,6 +918,22 @@ public class FingerprintManager implements BiometricAuthenticator, BiometricFing } } + /** + * @hide + */ + @RequiresPermission(USE_BIOMETRIC_INTERNAL) + public void setUdfpsOverlay(@NonNull IUdfpsOverlay controller) { + if (mService == null) { + Slog.w(TAG, "setUdfpsOverlay: no fingerprint service"); + return; + } + + try { + mService.setUdfpsOverlay(controller); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } /** * Forwards BiometricStateListener to FingerprintService diff --git a/core/java/android/hardware/fingerprint/IFingerprintService.aidl b/core/java/android/hardware/fingerprint/IFingerprintService.aidl index 051e3a4caa4e..365a6b3e06e5 100644 --- a/core/java/android/hardware/fingerprint/IFingerprintService.aidl +++ b/core/java/android/hardware/fingerprint/IFingerprintService.aidl @@ -26,6 +26,7 @@ import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback import android.hardware.fingerprint.IFingerprintServiceReceiver; import android.hardware.fingerprint.IUdfpsOverlayController; import android.hardware.fingerprint.ISidefpsController; +import android.hardware.fingerprint.IUdfpsOverlay; import android.hardware.fingerprint.Fingerprint; import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; import java.util.List; @@ -201,6 +202,10 @@ interface IFingerprintService { @EnforcePermission("USE_BIOMETRIC_INTERNAL") void setSidefpsController(in ISidefpsController controller); + // Sets the controller for managing the UDFPS overlay. + @EnforcePermission("USE_BIOMETRIC_INTERNAL") + void setUdfpsOverlay(in IUdfpsOverlay controller); + // Registers BiometricStateListener. @EnforcePermission("USE_BIOMETRIC_INTERNAL") void registerBiometricStateListener(IBiometricStateListener listener); diff --git a/core/java/android/hardware/fingerprint/IUdfpsOverlay.aidl b/core/java/android/hardware/fingerprint/IUdfpsOverlay.aidl new file mode 100644 index 000000000000..c99fcccc68ea --- /dev/null +++ b/core/java/android/hardware/fingerprint/IUdfpsOverlay.aidl @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.hardware.fingerprint; + +/** + * Interface for interacting with the under-display fingerprint sensor (UDFPS) overlay. + * @hide + */ +oneway interface IUdfpsOverlay { + // Shows the overlay. + void show(long requestId, int sensorId, int reason); + + // Hides the overlay. + void hide(int sensorId); +} diff --git a/core/java/android/hardware/input/IInputManager.aidl b/core/java/android/hardware/input/IInputManager.aidl index f213224b981e..49c0f9261c53 100644 --- a/core/java/android/hardware/input/IInputManager.aidl +++ b/core/java/android/hardware/input/IInputManager.aidl @@ -161,4 +161,11 @@ interface IInputManager { void registerBatteryListener(int deviceId, IInputDeviceBatteryListener listener); void unregisterBatteryListener(int deviceId, IInputDeviceBatteryListener listener); + + // Get the bluetooth address of an input device if known, returning null if it either is not + // connected via bluetooth or if the address cannot be determined. + @EnforcePermission("BLUETOOTH") + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = " + + "android.Manifest.permission.BLUETOOTH)") + String getInputDeviceBluetoothAddress(int deviceId); } diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java index 8d4aac4bba88..0cf15f76103e 100644 --- a/core/java/android/hardware/input/InputManager.java +++ b/core/java/android/hardware/input/InputManager.java @@ -1481,6 +1481,24 @@ public final class InputManager { } /** + * Returns the Bluetooth address of this input device, if known. + * + * The returned string is always null if this input device is not connected + * via Bluetooth, or if the Bluetooth address of the device cannot be + * determined. The returned address will look like: "11:22:33:44:55:66". + * @hide + */ + @RequiresPermission(Manifest.permission.BLUETOOTH) + @Nullable + public String getInputDeviceBluetoothAddress(int deviceId) { + try { + return mIm.getInputDeviceBluetoothAddress(deviceId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * Gets a vibrator service associated with an input device, always creates a new instance. * @return The vibrator, never null. * @hide diff --git a/core/java/android/hardware/usb/UsbDeviceConnection.java b/core/java/android/hardware/usb/UsbDeviceConnection.java index 60d8cacd19be..7c2e518b8544 100644 --- a/core/java/android/hardware/usb/UsbDeviceConnection.java +++ b/core/java/android/hardware/usb/UsbDeviceConnection.java @@ -108,6 +108,34 @@ public class UsbDeviceConnection { } /** + * This is meant to be called by UsbRequest's queue() in order to synchronize on + * UsbDeviceConnection's mLock to prevent the connection being closed while queueing. + */ + /* package */ boolean queueRequest(UsbRequest request, ByteBuffer buffer, int length) { + synchronized (mLock) { + if (!isOpen()) { + return false; + } + + return request.queueIfConnectionOpen(buffer, length); + } + } + + /** + * This is meant to be called by UsbRequest's queue() in order to synchronize on + * UsbDeviceConnection's mLock to prevent the connection being closed while queueing. + */ + /* package */ boolean queueRequest(UsbRequest request, @Nullable ByteBuffer buffer) { + synchronized (mLock) { + if (!isOpen()) { + return false; + } + + return request.queueIfConnectionOpen(buffer); + } + } + + /** * Releases all system resources related to the device. * Once the object is closed it cannot be used again. * The client must call {@link UsbManager#openDevice} again diff --git a/core/java/android/hardware/usb/UsbRequest.java b/core/java/android/hardware/usb/UsbRequest.java index 6ac5e8de8fa7..beb0f8d336d5 100644 --- a/core/java/android/hardware/usb/UsbRequest.java +++ b/core/java/android/hardware/usb/UsbRequest.java @@ -113,11 +113,13 @@ public class UsbRequest { * Releases all resources related to this request. */ public void close() { - if (mNativeContext != 0) { - mEndpoint = null; - mConnection = null; - native_close(); - mCloseGuard.close(); + synchronized (mLock) { + if (mNativeContext != 0) { + mEndpoint = null; + mConnection = null; + native_close(); + mCloseGuard.close(); + } } } @@ -191,10 +193,32 @@ public class UsbRequest { */ @Deprecated public boolean queue(ByteBuffer buffer, int length) { + UsbDeviceConnection connection = mConnection; + if (connection == null) { + // The expected exception by CTS Verifier - USB Device test + throw new NullPointerException("invalid connection"); + } + + // Calling into the underlying UsbDeviceConnection to synchronize on its lock, to prevent + // the connection being closed while queueing. + return connection.queueRequest(this, buffer, length); + } + + /** + * This is meant to be called from UsbDeviceConnection after synchronizing using the lock over + * there, to prevent the connection being closed while queueing. + */ + /* package */ boolean queueIfConnectionOpen(ByteBuffer buffer, int length) { + UsbDeviceConnection connection = mConnection; + if (connection == null || !connection.isOpen()) { + // The expected exception by CTS Verifier - USB Device test + throw new NullPointerException("invalid connection"); + } + boolean out = (mEndpoint.getDirection() == UsbConstants.USB_DIR_OUT); boolean result; - if (mConnection.getContext().getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.P + if (connection.getContext().getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.P && length > MAX_USBFS_BUFFER_SIZE) { length = MAX_USBFS_BUFFER_SIZE; } @@ -243,6 +267,28 @@ public class UsbRequest { * @return true if the queueing operation succeeded */ public boolean queue(@Nullable ByteBuffer buffer) { + UsbDeviceConnection connection = mConnection; + if (connection == null) { + // The expected exception by CTS Verifier - USB Device test + throw new IllegalStateException("invalid connection"); + } + + // Calling into the underlying UsbDeviceConnection to synchronize on its lock, to prevent + // the connection being closed while queueing. + return connection.queueRequest(this, buffer); + } + + /** + * This is meant to be called from UsbDeviceConnection after synchronizing using the lock over + * there, to prevent the connection being closed while queueing. + */ + /* package */ boolean queueIfConnectionOpen(@Nullable ByteBuffer buffer) { + UsbDeviceConnection connection = mConnection; + if (connection == null || !connection.isOpen()) { + // The expected exception by CTS Verifier - USB Device test + throw new IllegalStateException("invalid connection"); + } + // Request need to be initialized Preconditions.checkState(mNativeContext != 0, "request is not initialized"); @@ -260,7 +306,7 @@ public class UsbRequest { mIsUsingNewQueue = true; wasQueued = native_queue(null, 0, 0); } else { - if (mConnection.getContext().getApplicationInfo().targetSdkVersion + if (connection.getContext().getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.P) { // Can only send/receive MAX_USBFS_BUFFER_SIZE bytes at once Preconditions.checkArgumentInRange(buffer.remaining(), 0, MAX_USBFS_BUFFER_SIZE, @@ -363,11 +409,12 @@ public class UsbRequest { * @return true if cancelling succeeded */ public boolean cancel() { - if (mConnection == null) { + UsbDeviceConnection connection = mConnection; + if (connection == null) { return false; } - return mConnection.cancelRequest(this); + return connection.cancelRequest(this); } /** @@ -382,7 +429,8 @@ public class UsbRequest { * @return true if cancelling succeeded. */ /* package */ boolean cancelIfOpen() { - if (mNativeContext == 0 || (mConnection != null && !mConnection.isOpen())) { + UsbDeviceConnection connection = mConnection; + if (mNativeContext == 0 || (connection != null && !connection.isOpen())) { Log.w(TAG, "Detected attempt to cancel a request on a connection which isn't open"); return false; diff --git a/core/java/android/os/Binder.java b/core/java/android/os/Binder.java index d3a6323230a5..3c4abab346a4 100644 --- a/core/java/android/os/Binder.java +++ b/core/java/android/os/Binder.java @@ -562,7 +562,7 @@ public class Binder implements IBinder { * * @hide */ - @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) public final native void markVintfStability(); /** @@ -1219,25 +1219,40 @@ public class Binder implements IBinder { @UnsupportedAppUsage private boolean execTransact(int code, long dataObj, long replyObj, int flags) { + + Parcel data = Parcel.obtain(dataObj); + Parcel reply = Parcel.obtain(replyObj); + // At that point, the parcel request headers haven't been parsed so we do not know what // {@link WorkSource} the caller has set. Use calling UID as the default. - final int callingUid = Binder.getCallingUid(); - final long origWorkSource = ThreadLocalWorkSource.setUid(callingUid); + // + // TODO: this is wrong - we should attribute along the entire call route + // also this attribution logic should move to native code - it only works + // for Java now + // + // This attribution support is not generic and therefore not support in RPC mode + final int callingUid = data.isForRpc() ? -1 : Binder.getCallingUid(); + final long origWorkSource = callingUid == -1 + ? -1 : ThreadLocalWorkSource.setUid(callingUid); + try { - return execTransactInternal(code, dataObj, replyObj, flags, callingUid); + return execTransactInternal(code, data, reply, flags, callingUid); } finally { - ThreadLocalWorkSource.restore(origWorkSource); + reply.recycle(); + data.recycle(); + + if (callingUid != -1) { + ThreadLocalWorkSource.restore(origWorkSource); + } } } - private boolean execTransactInternal(int code, long dataObj, long replyObj, int flags, + private boolean execTransactInternal(int code, Parcel data, Parcel reply, int flags, int callingUid) { // Make sure the observer won't change while processing a transaction. final BinderInternal.Observer observer = sObserver; final CallSession callSession = observer != null ? observer.callStarted(this, code, UNSET_WORKSOURCE) : null; - Parcel data = Parcel.obtain(dataObj); - Parcel reply = Parcel.obtain(replyObj); // Theoretically, we should call transact, which will call onTransact, // but all that does is rewind it, and we just got these from an IPC, // so we'll just call it directly. @@ -1268,8 +1283,10 @@ public class Binder implements IBinder { final boolean tracingEnabled = tagEnabled && transactionTraceName != null; try { + // TODO - this logic should not be in Java - it should be in native + // code in libbinder so that it works for all binder users. final BinderCallHeavyHitterWatcher heavyHitterWatcher = sHeavyHitterWatcher; - if (heavyHitterWatcher != null) { + if (heavyHitterWatcher != null && callingUid != -1) { // Notify the heavy hitter watcher, if it's enabled. heavyHitterWatcher.onTransaction(callingUid, getClass(), code); } @@ -1277,7 +1294,10 @@ public class Binder implements IBinder { Trace.traceBegin(Trace.TRACE_TAG_AIDL, transactionTraceName); } - if ((flags & FLAG_COLLECT_NOTED_APP_OPS) != 0) { + // TODO - this logic should not be in Java - it should be in native + // code in libbinder so that it works for all binder users. Further, + // this should not re-use flags. + if ((flags & FLAG_COLLECT_NOTED_APP_OPS) != 0 && callingUid != -1) { AppOpsManager.startNotedAppOpsCollection(callingUid); try { res = onTransact(code, data, reply, flags); @@ -1320,8 +1340,6 @@ public class Binder implements IBinder { } checkParcel(this, code, reply, "Unreasonably large binder reply buffer"); - reply.recycle(); - data.recycle(); } // Just in case -- we are done with the IPC, so there should be no more strict diff --git a/core/java/android/os/BundleMerger.java b/core/java/android/os/BundleMerger.java new file mode 100644 index 000000000000..51bd4ea75005 --- /dev/null +++ b/core/java/android/os/BundleMerger.java @@ -0,0 +1,379 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.os; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Objects; +import java.util.function.BinaryOperator; + +/** + * Configured rules for merging two {@link Bundle} instances. + * <p> + * By default, values from both {@link Bundle} instances are blended together on + * a key-wise basis, and conflicting value definitions for a key are dropped. + * <p> + * Nuanced strategies for handling conflicting value definitions can be applied + * using {@link #setMergeStrategy(String, int)} and + * {@link #setDefaultMergeStrategy(int)}. + * <p> + * When conflicting values have <em>inconsistent</em> data types (such as trying + * to merge a {@link String} and a {@link Integer}), both conflicting values are + * rejected and the key becomes undefined, regardless of the requested strategy. + * + * @hide + */ +public class BundleMerger implements Parcelable { + private static final String TAG = "BundleMerger"; + + private @Strategy int mDefaultStrategy = STRATEGY_REJECT; + + private final ArrayMap<String, Integer> mStrategies = new ArrayMap<>(); + + /** + * Merge strategy that rejects both conflicting values. + */ + public static final int STRATEGY_REJECT = 0; + + /** + * Merge strategy that selects the first of conflicting values. + */ + public static final int STRATEGY_FIRST = 1; + + /** + * Merge strategy that selects the last of conflicting values. + */ + public static final int STRATEGY_LAST = 2; + + /** + * Merge strategy that selects the "minimum" of conflicting values which are + * {@link Comparable} with each other. + */ + public static final int STRATEGY_COMPARABLE_MIN = 3; + + /** + * Merge strategy that selects the "maximum" of conflicting values which are + * {@link Comparable} with each other. + */ + public static final int STRATEGY_COMPARABLE_MAX = 4; + + /** + * Merge strategy that numerically adds both conflicting values. + */ + public static final int STRATEGY_NUMBER_ADD = 5; + + /** + * Merge strategy that numerically increments the first conflicting value by + * {@code 1} and ignores the last conflicting value. + */ + public static final int STRATEGY_NUMBER_INCREMENT_FIRST = 6; + + /** + * Merge strategy that combines conflicting values using a boolean "and" + * operation. + */ + public static final int STRATEGY_BOOLEAN_AND = 7; + + /** + * Merge strategy that combines conflicting values using a boolean "or" + * operation. + */ + public static final int STRATEGY_BOOLEAN_OR = 8; + + /** + * Merge strategy that combines two conflicting array values by appending + * the last array after the first array. + */ + public static final int STRATEGY_ARRAY_APPEND = 9; + + /** + * Merge strategy that combines two conflicting {@link ArrayList} values by + * appending the last {@link ArrayList} after the first {@link ArrayList}. + */ + public static final int STRATEGY_ARRAY_LIST_APPEND = 10; + + @IntDef(flag = false, prefix = { "STRATEGY_" }, value = { + STRATEGY_REJECT, + STRATEGY_FIRST, + STRATEGY_LAST, + STRATEGY_COMPARABLE_MIN, + STRATEGY_COMPARABLE_MAX, + STRATEGY_NUMBER_ADD, + STRATEGY_NUMBER_INCREMENT_FIRST, + STRATEGY_BOOLEAN_AND, + STRATEGY_BOOLEAN_OR, + STRATEGY_ARRAY_APPEND, + STRATEGY_ARRAY_LIST_APPEND, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface Strategy {} + + /** + * Create a empty set of rules for merging two {@link Bundle} instances. + */ + public BundleMerger() { + } + + private BundleMerger(@NonNull Parcel in) { + mDefaultStrategy = in.readInt(); + final int N = in.readInt(); + for (int i = 0; i < N; i++) { + mStrategies.put(in.readString(), in.readInt()); + } + } + + @Override + public void writeToParcel(@NonNull Parcel out, int flags) { + out.writeInt(mDefaultStrategy); + final int N = mStrategies.size(); + out.writeInt(N); + for (int i = 0; i < N; i++) { + out.writeString(mStrategies.keyAt(i)); + out.writeInt(mStrategies.valueAt(i)); + } + } + + @Override + public int describeContents() { + return 0; + } + + /** + * Configure the default merge strategy to be used when there isn't a + * more-specific strategy defined for a particular key via + * {@link #setMergeStrategy(String, int)}. + */ + public void setDefaultMergeStrategy(@Strategy int strategy) { + mDefaultStrategy = strategy; + } + + /** + * Configure the merge strategy to be used for the given key. + * <p> + * Subsequent calls for the same key will overwrite any previously + * configured strategy. + */ + public void setMergeStrategy(@NonNull String key, @Strategy int strategy) { + mStrategies.put(key, strategy); + } + + /** + * Return the merge strategy to be used for the given key, as defined by + * {@link #setMergeStrategy(String, int)}. + * <p> + * If no specific strategy has been configured for the given key, this + * returns {@link #setDefaultMergeStrategy(int)}. + */ + public @Strategy int getMergeStrategy(@NonNull String key) { + return (int) mStrategies.getOrDefault(key, mDefaultStrategy); + } + + /** + * Return a {@link BinaryOperator} which applies the strategies configured + * in this object to merge the two given {@link Bundle} arguments. + */ + public BinaryOperator<Bundle> asBinaryOperator() { + return this::merge; + } + + /** + * Apply the strategies configured in this object to merge the two given + * {@link Bundle} arguments. + * + * @return the merged {@link Bundle} result. If one argument is {@code null} + * it will return the other argument. If both arguments are null it + * will return {@code null}. + */ + @SuppressWarnings("deprecation") + public @Nullable Bundle merge(@Nullable Bundle first, @Nullable Bundle last) { + if (first == null && last == null) { + return null; + } + if (first == null) { + first = Bundle.EMPTY; + } + if (last == null) { + last = Bundle.EMPTY; + } + + // Start by bulk-copying all values without attempting to unpack any + // custom parcelables; we'll circle back to handle conflicts below + final Bundle res = new Bundle(); + res.putAll(first); + res.putAll(last); + + final ArraySet<String> conflictingKeys = new ArraySet<>(); + conflictingKeys.addAll(first.keySet()); + conflictingKeys.retainAll(last.keySet()); + for (int i = 0; i < conflictingKeys.size(); i++) { + final String key = conflictingKeys.valueAt(i); + final int strategy = getMergeStrategy(key); + final Object firstValue = first.get(key); + final Object lastValue = last.get(key); + try { + res.putObject(key, merge(strategy, firstValue, lastValue)); + } catch (Exception e) { + Log.w(TAG, "Failed to merge key " + key + " with " + firstValue + " and " + + lastValue + " using strategy " + strategy, e); + } + } + return res; + } + + /** + * Merge the two given values. If only one of the values is defined, it + * always wins, otherwise the given strategy is applied. + * + * @hide + */ + @VisibleForTesting + public static @Nullable Object merge(@Strategy int strategy, + @Nullable Object first, @Nullable Object last) { + if (first == null) return last; + if (last == null) return first; + + if (first.getClass() != last.getClass()) { + throw new IllegalArgumentException("Merging requires consistent classes; first " + + first.getClass() + " last " + last.getClass()); + } + + switch (strategy) { + case STRATEGY_REJECT: + // Only actually reject when the values are different + if (Objects.deepEquals(first, last)) { + return first; + } else { + return null; + } + case STRATEGY_FIRST: + return first; + case STRATEGY_LAST: + return last; + case STRATEGY_COMPARABLE_MIN: + return comparableMin(first, last); + case STRATEGY_COMPARABLE_MAX: + return comparableMax(first, last); + case STRATEGY_NUMBER_ADD: + return numberAdd(first, last); + case STRATEGY_NUMBER_INCREMENT_FIRST: + return numberIncrementFirst(first, last); + case STRATEGY_BOOLEAN_AND: + return booleanAnd(first, last); + case STRATEGY_BOOLEAN_OR: + return booleanOr(first, last); + case STRATEGY_ARRAY_APPEND: + return arrayAppend(first, last); + case STRATEGY_ARRAY_LIST_APPEND: + return arrayListAppend(first, last); + default: + throw new UnsupportedOperationException(); + } + } + + @SuppressWarnings("unchecked") + private static @NonNull Object comparableMin(@NonNull Object first, @NonNull Object last) { + return ((Comparable<Object>) first).compareTo(last) < 0 ? first : last; + } + + @SuppressWarnings("unchecked") + private static @NonNull Object comparableMax(@NonNull Object first, @NonNull Object last) { + return ((Comparable<Object>) first).compareTo(last) >= 0 ? first : last; + } + + private static @NonNull Object numberAdd(@NonNull Object first, @NonNull Object last) { + if (first instanceof Integer) { + return ((Integer) first) + ((Integer) last); + } else if (first instanceof Long) { + return ((Long) first) + ((Long) last); + } else if (first instanceof Float) { + return ((Float) first) + ((Float) last); + } else if (first instanceof Double) { + return ((Double) first) + ((Double) last); + } else { + throw new IllegalArgumentException("Unable to add " + first.getClass()); + } + } + + private static @NonNull Number numberIncrementFirst(@NonNull Object first, + @NonNull Object last) { + if (first instanceof Integer) { + return ((Integer) first) + 1; + } else if (first instanceof Long) { + return ((Long) first) + 1L; + } else { + throw new IllegalArgumentException("Unable to add " + first.getClass()); + } + } + + private static @NonNull Object booleanAnd(@NonNull Object first, @NonNull Object last) { + return ((Boolean) first) && ((Boolean) last); + } + + private static @NonNull Object booleanOr(@NonNull Object first, @NonNull Object last) { + return ((Boolean) first) || ((Boolean) last); + } + + private static @NonNull Object arrayAppend(@NonNull Object first, @NonNull Object last) { + if (!first.getClass().isArray()) { + throw new IllegalArgumentException("Unable to append " + first.getClass()); + } + final Class<?> clazz = first.getClass().getComponentType(); + final int firstLength = Array.getLength(first); + final int lastLength = Array.getLength(last); + final Object res = Array.newInstance(clazz, firstLength + lastLength); + System.arraycopy(first, 0, res, 0, firstLength); + System.arraycopy(last, 0, res, firstLength, lastLength); + return res; + } + + @SuppressWarnings("unchecked") + private static @NonNull Object arrayListAppend(@NonNull Object first, @NonNull Object last) { + if (!(first instanceof ArrayList)) { + throw new IllegalArgumentException("Unable to append " + first.getClass()); + } + final ArrayList<Object> firstList = (ArrayList<Object>) first; + final ArrayList<Object> lastList = (ArrayList<Object>) last; + final ArrayList<Object> res = new ArrayList<>(firstList.size() + lastList.size()); + res.addAll(firstList); + res.addAll(lastList); + return res; + } + + public static final @android.annotation.NonNull Parcelable.Creator<BundleMerger> CREATOR = + new Parcelable.Creator<BundleMerger>() { + @Override + public BundleMerger createFromParcel(Parcel in) { + return new BundleMerger(in); + } + + @Override + public BundleMerger[] newArray(int size) { + return new BundleMerger[size]; + } + }; +} diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java index d451765f022f..2afa87980c11 100644 --- a/core/java/android/os/Parcel.java +++ b/core/java/android/os/Parcel.java @@ -367,6 +367,8 @@ public final class Parcel { @FastNative private static native void nativeMarkForBinder(long nativePtr, IBinder binder); @CriticalNative + private static native boolean nativeIsForRpc(long nativePtr); + @CriticalNative private static native int nativeDataSize(long nativePtr); @CriticalNative private static native int nativeDataAvail(long nativePtr); @@ -644,6 +646,15 @@ public final class Parcel { nativeMarkForBinder(mNativePtr, binder); } + /** + * Whether this Parcel is written for an RPC transaction. + * + * @hide + */ + public final boolean isForRpc() { + return nativeIsForRpc(mNativePtr); + } + /** @hide */ @ParcelFlags @TestApi diff --git a/core/java/android/os/PermissionEnforcer.java b/core/java/android/os/PermissionEnforcer.java new file mode 100644 index 000000000000..221e89a6a76f --- /dev/null +++ b/core/java/android/os/PermissionEnforcer.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.os; + +import android.annotation.NonNull; +import android.annotation.SystemService; +import android.content.AttributionSource; +import android.content.Context; +import android.content.PermissionChecker; +import android.permission.PermissionCheckerManager; + +/** + * PermissionEnforcer check permissions for AIDL-generated services which use + * the @EnforcePermission annotation. + * + * <p>AIDL services may be annotated with @EnforcePermission which will trigger + * the generation of permission check code. This generated code relies on + * PermissionEnforcer to validate the permissions. The methods available are + * purposely similar to the AIDL annotation syntax. + * + * @see android.permission.PermissionManager + * + * @hide + */ +@SystemService(Context.PERMISSION_ENFORCER_SERVICE) +public class PermissionEnforcer { + + private final Context mContext; + + /** Protected constructor. Allows subclasses to instantiate an object + * without using a Context. + */ + protected PermissionEnforcer() { + mContext = null; + } + + /** Constructor, prefer using the fromContext static method when possible */ + public PermissionEnforcer(@NonNull Context context) { + mContext = context; + } + + @PermissionCheckerManager.PermissionResult + protected int checkPermission(@NonNull String permission, @NonNull AttributionSource source) { + return PermissionChecker.checkPermissionForDataDelivery( + mContext, permission, PermissionChecker.PID_UNKNOWN, source, "" /* message */); + } + + public void enforcePermission(@NonNull String permission, @NonNull + AttributionSource source) throws SecurityException { + int result = checkPermission(permission, source); + if (result != PermissionCheckerManager.PERMISSION_GRANTED) { + throw new SecurityException("Access denied, requires: " + permission); + } + } + + public void enforcePermissionAllOf(@NonNull String[] permissions, + @NonNull AttributionSource source) throws SecurityException { + for (String permission : permissions) { + int result = checkPermission(permission, source); + if (result != PermissionCheckerManager.PERMISSION_GRANTED) { + throw new SecurityException("Access denied, requires: allOf={" + + String.join(", ", permissions) + "}"); + } + } + } + + public void enforcePermissionAnyOf(@NonNull String[] permissions, + @NonNull AttributionSource source) throws SecurityException { + for (String permission : permissions) { + int result = checkPermission(permission, source); + if (result == PermissionCheckerManager.PERMISSION_GRANTED) { + return; + } + } + throw new SecurityException("Access denied, requires: anyOf={" + + String.join(", ", permissions) + "}"); + } + + /** + * Returns a new PermissionEnforcer based on a Context. + * + * @hide + */ + public static PermissionEnforcer fromContext(@NonNull Context context) { + return context.getSystemService(PermissionEnforcer.class); + } +} diff --git a/core/java/android/os/ServiceManagerNative.java b/core/java/android/os/ServiceManagerNative.java index 2dcf67483203..f2143f63d1ee 100644 --- a/core/java/android/os/ServiceManagerNative.java +++ b/core/java/android/os/ServiceManagerNative.java @@ -98,6 +98,10 @@ class ServiceManagerProxy implements IServiceManager { return mServiceManager.updatableViaApex(name); } + public String[] getUpdatableNames(String apexName) throws RemoteException { + return mServiceManager.getUpdatableNames(apexName); + } + public ConnectionInfo getConnectionInfo(String name) throws RemoteException { return mServiceManager.getConnectionInfo(name); } diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index cd2bbebf3d4d..897b7c3afe54 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -3374,9 +3374,26 @@ public final class Settings { } } - // Fetch all flags for the namespace at once for caching purposes - Bundle b = cp.call(cr.getAttributionSource(), - mProviderHolder.mUri.getAuthority(), mCallListCommand, null, args); + Bundle b; + // b/252663068: if we're in system server and the caller did not call + // clearCallingIdentity, the read would fail due to mismatched AttributionSources. + // TODO(b/256013480): remove this bypass after fixing the callers in system server. + if (namespace.equals(DeviceConfig.NAMESPACE_DEVICE_POLICY_MANAGER) + && Settings.isInSystemServer() + && Binder.getCallingUid() != Process.myUid()) { + final long token = Binder.clearCallingIdentity(); + try { + // Fetch all flags for the namespace at once for caching purposes + b = cp.call(cr.getAttributionSource(), + mProviderHolder.mUri.getAuthority(), mCallListCommand, null, args); + } finally { + Binder.restoreCallingIdentity(token); + } + } else { + // Fetch all flags for the namespace at once for caching purposes + b = cp.call(cr.getAttributionSource(), + mProviderHolder.mUri.getAuthority(), mCallListCommand, null, args); + } if (b == null) { // Invalid response, return an empty map return keyValues; @@ -7135,7 +7152,7 @@ public final class Settings { * Format like "ime0;subtype0;subtype1;subtype2:ime1:ime2;subtype0" * where imeId is ComponentName and subtype is int32. */ - @Readable + @Readable(maxTargetSdk = Build.VERSION_CODES.TIRAMISU) public static final String ENABLED_INPUT_METHODS = "enabled_input_methods"; /** @@ -7144,7 +7161,7 @@ public final class Settings { * by ':'. * @hide */ - @Readable + @Readable(maxTargetSdk = Build.VERSION_CODES.TIRAMISU) public static final String DISABLED_SYSTEM_INPUT_METHODS = "disabled_system_input_methods"; /** diff --git a/core/java/android/service/credentials/CredentialEntry.java b/core/java/android/service/credentials/CredentialEntry.java index 4cc43a10e88f..a3fa979a6188 100644 --- a/core/java/android/service/credentials/CredentialEntry.java +++ b/core/java/android/service/credentials/CredentialEntry.java @@ -140,8 +140,8 @@ public final class CredentialEntry implements Parcelable { public static final class Builder { private String mType; private Slice mSlice; - private PendingIntent mPendingIntent; - private Credential mCredential; + private PendingIntent mPendingIntent = null; + private Credential mCredential = null; private boolean mAutoSelectAllowed = false; /** diff --git a/core/java/android/service/credentials/CredentialProviderException.java b/core/java/android/service/credentials/CredentialProviderException.java index b39b4a0cc180..06f0052a29a9 100644 --- a/core/java/android/service/credentials/CredentialProviderException.java +++ b/core/java/android/service/credentials/CredentialProviderException.java @@ -30,6 +30,22 @@ import java.lang.annotation.RetentionPolicy; public class CredentialProviderException extends Exception { public static final int ERROR_UNKNOWN = 0; + /** + * For internal use only. + * Error code to be used when the provider request times out. + * + * @hide + */ + public static final int ERROR_TIMEOUT = 1; + + /** + * For internal use only. + * Error code to be used when the async task is canceled internally. + * + * @hide + */ + public static final int ERROR_TASK_CANCELED = 2; + private final int mErrorCode; /** @@ -37,6 +53,8 @@ public class CredentialProviderException extends Exception { */ @IntDef(prefix = {"ERROR_"}, value = { ERROR_UNKNOWN, + ERROR_TIMEOUT, + ERROR_TASK_CANCELED }) @Retention(RetentionPolicy.SOURCE) public @interface CredentialProviderError { } diff --git a/core/java/android/service/credentials/CredentialProviderInfo.java b/core/java/android/service/credentials/CredentialProviderInfo.java index e3f8cb7bb23e..2c7a983826f6 100644 --- a/core/java/android/service/credentials/CredentialProviderInfo.java +++ b/core/java/android/service/credentials/CredentialProviderInfo.java @@ -18,16 +18,21 @@ package android.service.credentials; import android.Manifest; import android.annotation.NonNull; +import android.annotation.Nullable; import android.annotation.UserIdInt; import android.app.AppGlobals; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.pm.PackageItemInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.os.Bundle; import android.os.RemoteException; +import android.text.TextUtils; import android.util.Log; import android.util.Slog; @@ -48,19 +53,21 @@ public final class CredentialProviderInfo { @NonNull private final List<String> mCapabilities; - // TODO: Move the two strings below to CredentialProviderService when ready. - private static final String CAPABILITY_META_DATA_KEY = "android.credentials.capabilities"; - private static final String SERVICE_INTERFACE = - "android.service.credentials.CredentialProviderService"; - + @NonNull + private final Context mContext; + @Nullable + private final Drawable mIcon; + @Nullable + private final CharSequence mLabel; /** * Constructs an information instance of the credential provider. * - * @param context The context object - * @param serviceComponent The serviceComponent of the provider service - * @param userId The android userId for which the current process is running + * @param context the context object + * @param serviceComponent the serviceComponent of the provider service + * @param userId the android userId for which the current process is running * @throws PackageManager.NameNotFoundException If provider service is not found + * @throws SecurityException If provider does not require the relevant permission */ public CredentialProviderInfo(@NonNull Context context, @NonNull ComponentName serviceComponent, int userId) @@ -68,7 +75,13 @@ public final class CredentialProviderInfo { this(context, getServiceInfoOrThrow(serviceComponent, userId)); } - private CredentialProviderInfo(@NonNull Context context, @NonNull ServiceInfo serviceInfo) { + /** + * Constructs an information instance of the credential provider. + * @param context the context object + * @param serviceInfo the service info for the provider app. This must be retrieved from the + * {@code PackageManager} + */ + public CredentialProviderInfo(@NonNull Context context, @NonNull ServiceInfo serviceInfo) { if (!Manifest.permission.BIND_CREDENTIAL_PROVIDER_SERVICE.equals(serviceInfo.permission)) { Log.i(TAG, "Credential Provider Service from : " + serviceInfo.packageName + "does not require permission" @@ -76,32 +89,43 @@ public final class CredentialProviderInfo { throw new SecurityException("Service does not require the expected permission : " + Manifest.permission.BIND_CREDENTIAL_PROVIDER_SERVICE); } + mContext = context; mServiceInfo = serviceInfo; mCapabilities = new ArrayList<>(); - populateProviderCapabilities(context); + mIcon = mServiceInfo.loadIcon(mContext.getPackageManager()); + mLabel = mServiceInfo.loadSafeLabel( + mContext.getPackageManager(), 0 /* do not ellipsize */, + TextUtils.SAFE_STRING_FLAG_FIRST_LINE | TextUtils.SAFE_STRING_FLAG_TRIM); + populateProviderCapabilities(context, serviceInfo); } - private void populateProviderCapabilities(@NonNull Context context) { - if (mServiceInfo.applicationInfo.metaData == null) { - return; - } + private void populateProviderCapabilities(@NonNull Context context, ServiceInfo serviceInfo) { + final PackageManager pm = context.getPackageManager(); try { - final int resourceId = mServiceInfo.applicationInfo.metaData.getInt( - CAPABILITY_META_DATA_KEY); - String[] capabilities = context.getResources().getStringArray(resourceId); - if (capabilities == null) { - Log.w(TAG, "No capabilities found for provider: " + mServiceInfo.packageName); + Bundle metadata = serviceInfo.metaData; + Resources resources = pm.getResourcesForApplication(serviceInfo.applicationInfo); + if (metadata == null || resources == null) { + Log.i(TAG, "populateProviderCapabilities - metadata or resources is null"); + return; + } + + String[] capabilities = resources.getStringArray(metadata.getInt( + CredentialProviderService.CAPABILITY_META_DATA_KEY)); + if (capabilities == null || capabilities.length == 0) { + Slog.i(TAG, "No capabilities found for provider:" + serviceInfo.packageName); return; } + for (String capability : capabilities) { if (capability.isEmpty()) { - Log.w(TAG, "Skipping empty capability"); + Slog.i(TAG, "Skipping empty capability"); continue; } + Slog.i(TAG, "Capabilities found for provider: " + capability); mCapabilities.add(capability); } - } catch (Resources.NotFoundException e) { - Log.w(TAG, "Exception while populating provider capabilities: " + e.getMessage()); + } catch (PackageManager.NameNotFoundException e) { + Slog.i(TAG, e.getMessage()); } } @@ -135,6 +159,18 @@ public final class CredentialProviderInfo { return mServiceInfo; } + /** Returns the service icon. */ + @Nullable + public Drawable getServiceIcon() { + return mIcon; + } + + /** Returns the service label. */ + @Nullable + public CharSequence getServiceLabel() { + return mLabel; + } + /** Returns an immutable list of capabilities this provider service can support. */ @NonNull public List<String> getCapabilities() { @@ -145,14 +181,15 @@ public final class CredentialProviderInfo { * Returns the valid credential provider services available for the user with the * given {@code userId}. */ + @NonNull public static List<CredentialProviderInfo> getAvailableServices(@NonNull Context context, @UserIdInt int userId) { final List<CredentialProviderInfo> services = new ArrayList<>(); final List<ResolveInfo> resolveInfos = context.getPackageManager().queryIntentServicesAsUser( - new Intent(SERVICE_INTERFACE), - PackageManager.GET_META_DATA, + new Intent(CredentialProviderService.SERVICE_INTERFACE), + PackageManager.ResolveInfoFlags.of(PackageManager.GET_META_DATA), userId); for (ResolveInfo resolveInfo : resolveInfos) { final ServiceInfo serviceInfo = resolveInfo.serviceInfo; @@ -169,8 +206,9 @@ public final class CredentialProviderInfo { * Returns the valid credential provider services available for the user, that can * support the given {@code credentialType}. */ + @NonNull public static List<CredentialProviderInfo> getAvailableServicesForCapability( - Context context, @UserIdInt int userId, String credentialType) { + @NonNull Context context, @UserIdInt int userId, @NonNull String credentialType) { List<CredentialProviderInfo> servicesForCapability = new ArrayList<>(); final List<CredentialProviderInfo> services = getAvailableServices(context, userId); diff --git a/core/java/android/service/credentials/CredentialProviderService.java b/core/java/android/service/credentials/CredentialProviderService.java index 1cdf186d898c..b1b08f466622 100644 --- a/core/java/android/service/credentials/CredentialProviderService.java +++ b/core/java/android/service/credentials/CredentialProviderService.java @@ -42,6 +42,9 @@ import java.util.Objects; */ public abstract class CredentialProviderService extends Service { private static final String TAG = "CredProviderService"; + + public static final String CAPABILITY_META_DATA_KEY = "android.credentials.capabilities"; + private Handler mHandler; /** @@ -71,12 +74,13 @@ public abstract class CredentialProviderService extends Service { private final ICredentialProviderService mInterface = new ICredentialProviderService.Stub() { @Override - public void onGetCredentials(GetCredentialsRequest request, ICancellationSignal transport, + public ICancellationSignal onGetCredentials(GetCredentialsRequest request, IGetCredentialsCallback callback) { Objects.requireNonNull(request); - Objects.requireNonNull(transport); Objects.requireNonNull(callback); + ICancellationSignal transport = CancellationSignal.createTransport(); + mHandler.sendMessage(obtainMessage( CredentialProviderService::onGetCredentials, CredentialProviderService.this, request, @@ -100,15 +104,17 @@ public abstract class CredentialProviderService extends Service { } } )); + return transport; } @Override - public void onCreateCredential(CreateCredentialRequest request, - ICancellationSignal transport, ICreateCredentialCallback callback) { + public ICancellationSignal onCreateCredential(CreateCredentialRequest request, + ICreateCredentialCallback callback) { Objects.requireNonNull(request); - Objects.requireNonNull(transport); Objects.requireNonNull(callback); + ICancellationSignal transport = CancellationSignal.createTransport(); + mHandler.sendMessage(obtainMessage( CredentialProviderService::onCreateCredential, CredentialProviderService.this, request, @@ -132,6 +138,7 @@ public abstract class CredentialProviderService extends Service { } } )); + return transport; } }; diff --git a/core/java/android/service/credentials/ICredentialProviderService.aidl b/core/java/android/service/credentials/ICredentialProviderService.aidl index c68430ce752e..c21cefab701a 100644 --- a/core/java/android/service/credentials/ICredentialProviderService.aidl +++ b/core/java/android/service/credentials/ICredentialProviderService.aidl @@ -21,13 +21,14 @@ import android.service.credentials.GetCredentialsRequest; import android.service.credentials.CreateCredentialRequest; import android.service.credentials.IGetCredentialsCallback; import android.service.credentials.ICreateCredentialCallback; +import android.os.ICancellationSignal; /** * Interface from the system to a credential provider service. * * @hide */ -oneway interface ICredentialProviderService { - void onGetCredentials(in GetCredentialsRequest request, in ICancellationSignal transport, in IGetCredentialsCallback callback); - void onCreateCredential(in CreateCredentialRequest request, in ICancellationSignal transport, in ICreateCredentialCallback callback); +interface ICredentialProviderService { + ICancellationSignal onGetCredentials(in GetCredentialsRequest request, in IGetCredentialsCallback callback); + ICancellationSignal onCreateCredential(in CreateCredentialRequest request, in ICreateCredentialCallback callback); } diff --git a/core/java/android/service/dreams/DreamManagerInternal.java b/core/java/android/service/dreams/DreamManagerInternal.java index 295171ca9bbd..5f30ad054c50 100644 --- a/core/java/android/service/dreams/DreamManagerInternal.java +++ b/core/java/android/service/dreams/DreamManagerInternal.java @@ -54,6 +54,13 @@ public abstract class DreamManagerInternal { public abstract void requestDream(); /** + * Whether dreaming can start given user settings and the current dock/charge state. + * + * @param isScreenOn True if the screen is currently on. + */ + public abstract boolean canStartDreaming(boolean isScreenOn); + + /** * Called by the ActivityTaskManagerService to verify that the startDreamActivity * request comes from the current active dream component. * diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java index a59d429b24e6..c6cd708c5967 100644 --- a/core/java/android/service/wallpaper/WallpaperService.java +++ b/core/java/android/service/wallpaper/WallpaperService.java @@ -81,7 +81,6 @@ import android.view.InputEvent; import android.view.InputEventReceiver; import android.view.InsetsSourceControl; import android.view.InsetsState; -import android.view.InsetsVisibilities; import android.view.MotionEvent; import android.view.PixelCopy; import android.view.Surface; @@ -251,7 +250,6 @@ public abstract class WallpaperService extends Service { final Rect mDispatchedStableInsets = new Rect(); DisplayCutout mDispatchedDisplayCutout = DisplayCutout.NO_CUTOUT; final InsetsState mInsetsState = new InsetsState(); - final InsetsVisibilities mRequestedVisibilities = new InsetsVisibilities(); final InsetsSourceControl[] mTempControls = new InsetsSourceControl[0]; final MergedConfiguration mMergedConfiguration = new MergedConfiguration(); final Bundle mSyncSeqIdBundle = new Bundle(); @@ -1133,8 +1131,9 @@ public abstract class WallpaperService extends Service { InputChannel inputChannel = new InputChannel(); if (mSession.addToDisplay(mWindow, mLayout, View.VISIBLE, - mDisplay.getDisplayId(), mRequestedVisibilities, inputChannel, - mInsetsState, mTempControls, new Rect(), new float[1]) < 0) { + mDisplay.getDisplayId(), WindowInsets.Type.defaultVisible(), + inputChannel, mInsetsState, mTempControls, new Rect(), + new float[1]) < 0) { Log.w(TAG, "Failed to add window while updating wallpaper surface."); return; } diff --git a/core/java/android/text/method/BaseKeyListener.java b/core/java/android/text/method/BaseKeyListener.java index d4bcd12abd96..01989d54b871 100644 --- a/core/java/android/text/method/BaseKeyListener.java +++ b/core/java/android/text/method/BaseKeyListener.java @@ -229,6 +229,8 @@ public abstract class BaseKeyListener extends MetaKeyKeyListener break; } else if (Emoji.isEmojiModifierBase(codePoint)) { deleteCharCount += Character.charCount(codePoint); + state = STATE_BEFORE_EMOJI; + break; } state = STATE_FINISHED; break; diff --git a/core/java/android/util/DisplayMetrics.java b/core/java/android/util/DisplayMetrics.java index 0a3e6b1cff38..517d98222093 100755 --- a/core/java/android/util/DisplayMetrics.java +++ b/core/java/android/util/DisplayMetrics.java @@ -174,6 +174,14 @@ public class DisplayMetrics { * This is not a density that applications should target, instead relying * on the system to scale their {@link #DENSITY_XXXHIGH} assets for them. */ + public static final int DENSITY_520 = 520; + + /** + * Intermediate density for screens that sit somewhere between + * {@link #DENSITY_XXHIGH} (480 dpi) and {@link #DENSITY_XXXHIGH} (640 dpi). + * This is not a density that applications should target, instead relying + * on the system to scale their {@link #DENSITY_XXXHIGH} assets for them. + */ public static final int DENSITY_560 = 560; /** diff --git a/core/java/android/view/IDisplayWindowInsetsController.aidl b/core/java/android/view/IDisplayWindowInsetsController.aidl index 1940042a1052..0769f1209a2b 100644 --- a/core/java/android/view/IDisplayWindowInsetsController.aidl +++ b/core/java/android/view/IDisplayWindowInsetsController.aidl @@ -19,7 +19,6 @@ package android.view; import android.content.ComponentName; import android.view.InsetsSourceControl; import android.view.InsetsState; -import android.view.InsetsVisibilities; /** * Singular controller of insets to use when there isn't another obvious controller available. @@ -32,10 +31,9 @@ oneway interface IDisplayWindowInsetsController { * Called when top focused window changes to determine whether or not to take over insets * control. Won't be called if config_remoteInsetsControllerControlsSystemBars is false. * @param component: Passes the top application component in the focused window. - * @param requestedVisibilities The insets visibilities requested by the focussed window. + * @param requestedVisibleTypes The insets types requested visible by the focused window. */ - void topFocusedWindowChanged(in ComponentName component, - in InsetsVisibilities insetsVisibilities); + void topFocusedWindowChanged(in ComponentName component, int requestedVisibleTypes); /** * @see IWindow#insetsChanged diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl index dddbe395aef1..e2bc5668d058 100644 --- a/core/java/android/view/IWindowManager.aidl +++ b/core/java/android/view/IWindowManager.aidl @@ -721,7 +721,7 @@ interface IWindowManager * Called when a remote process updates the requested visibilities of insets on a display window * container. */ - void updateDisplayWindowRequestedVisibilities(int displayId, in InsetsVisibilities vis); + void updateDisplayWindowRequestedVisibleTypes(int displayId, int requestedVisibleTypes); /** * Called to get the expected window insets. diff --git a/core/java/android/view/IWindowSession.aidl b/core/java/android/view/IWindowSession.aidl index 0052e82c2448..03ccb47305e0 100644 --- a/core/java/android/view/IWindowSession.aidl +++ b/core/java/android/view/IWindowSession.aidl @@ -32,7 +32,6 @@ import android.view.MotionEvent; import android.view.WindowManager; import android.view.InsetsSourceControl; import android.view.InsetsState; -import android.view.InsetsVisibilities; import android.view.Surface; import android.view.SurfaceControl; import android.view.SurfaceControl.Transaction; @@ -48,15 +47,15 @@ import java.util.List; */ interface IWindowSession { int addToDisplay(IWindow window, in WindowManager.LayoutParams attrs, - in int viewVisibility, in int layerStackId, in InsetsVisibilities requestedVisibilities, + in int viewVisibility, in int layerStackId, int requestedVisibleTypes, out InputChannel outInputChannel, out InsetsState insetsState, out InsetsSourceControl[] activeControls, out Rect attachedFrame, out float[] sizeCompatScale); int addToDisplayAsUser(IWindow window, in WindowManager.LayoutParams attrs, - in int viewVisibility, in int layerStackId, in int userId, - in InsetsVisibilities requestedVisibilities, out InputChannel outInputChannel, - out InsetsState insetsState, out InsetsSourceControl[] activeControls, - out Rect attachedFrame, out float[] sizeCompatScale); + in int viewVisibility, in int layerStackId, in int userId, int requestedVisibleTypes, + out InputChannel outInputChannel, out InsetsState insetsState, + out InsetsSourceControl[] activeControls, out Rect attachedFrame, + out float[] sizeCompatScale); int addToDisplayWithoutInputChannel(IWindow window, in WindowManager.LayoutParams attrs, in int viewVisibility, in int layerStackId, out InsetsState insetsState, out Rect attachedFrame, out float[] sizeCompatScale); @@ -279,9 +278,9 @@ interface IWindowSession { oneway void updateTapExcludeRegion(IWindow window, in Region region); /** - * Updates the requested visibilities of insets. + * Updates the requested visible types of insets. */ - oneway void updateRequestedVisibilities(IWindow window, in InsetsVisibilities visibilities); + oneway void updateRequestedVisibleTypes(IWindow window, int requestedVisibleTypes); /** * Called when the system gesture exclusion has changed. diff --git a/core/java/android/view/InputDevice.java b/core/java/android/view/InputDevice.java index 9b1d8673390b..799955b1107a 100644 --- a/core/java/android/view/InputDevice.java +++ b/core/java/android/view/InputDevice.java @@ -16,6 +16,7 @@ package android.view; +import android.Manifest; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -1010,6 +1011,22 @@ public final class InputDevice implements Parcelable { } /** + * Returns the Bluetooth address of this input device, if known. + * + * The returned string is always null if this input device is not connected + * via Bluetooth, or if the Bluetooth address of the device cannot be + * determined. The returned address will look like: "11:22:33:44:55:66". + * @hide + */ + @RequiresPermission(Manifest.permission.BLUETOOTH) + @Nullable + public String getBluetoothAddress() { + // We query the address via a separate InputManager API instead of pre-populating it in + // this class to avoid leaking it to apps that do not have sufficient permissions. + return InputManager.getInstance().getInputDeviceBluetoothAddress(mId); + } + + /** * Gets the vibrator service associated with the device, if there is one. * Even if the device does not have a vibrator, the result is never null. * Use {@link Vibrator#hasVibrator} to determine whether a vibrator is diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java index 4a72a62b7a8d..8b38e9e25001 100644 --- a/core/java/android/view/InsetsController.java +++ b/core/java/android/view/InsetsController.java @@ -46,6 +46,7 @@ import android.util.ArraySet; import android.util.Log; import android.util.Pair; import android.util.SparseArray; +import android.util.SparseIntArray; import android.util.proto.ProtoOutputStream; import android.view.InsetsSourceConsumer.ShowResult; import android.view.InsetsState.InternalInsetsType; @@ -102,18 +103,18 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation void applySurfaceParams(final SyncRtSurfaceTransactionApplier.SurfaceParams... params); /** - * @see ViewRootImpl#updateCompatSysUiVisibility(int, boolean, boolean) + * @see ViewRootImpl#updateCompatSysUiVisibility(int, int, int) */ - void updateCompatSysUiVisibility(@InternalInsetsType int type, boolean visible, - boolean hasControl); + default void updateCompatSysUiVisibility(@InsetsType int visibleTypes, + @InsetsType int requestedVisibleTypes, @InsetsType int controllableTypes) { } /** * Called when the requested visibilities of insets have been modified by the client. * The visibilities should be reported back to WM. * - * @param visibilities A collection of the requested visibilities. + * @param types Bitwise flags of types requested visible. */ - void updateRequestedVisibilities(InsetsVisibilities visibilities); + void updateRequestedVisibleTypes(@InsetsType int types); /** * @return Whether the host has any callbacks it wants to synchronize the animations with. @@ -564,9 +565,6 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation /** The state dispatched from server */ private final InsetsState mLastDispatchedState = new InsetsState(); - /** The requested visibilities sent to server */ - private final InsetsVisibilities mRequestedVisibilities = new InsetsVisibilities(); - private final Rect mFrame = new Rect(); private final BiFunction<InsetsController, Integer, InsetsSourceConsumer> mConsumerCreator; private final SparseArray<InsetsSourceConsumer> mSourceConsumers = new SparseArray<>(); @@ -575,7 +573,6 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation private final SparseArray<InsetsSourceControl> mTmpControlArray = new SparseArray<>(); private final ArrayList<RunningAnimation> mRunningAnimations = new ArrayList<>(); - private final ArraySet<InsetsSourceConsumer> mRequestedVisibilityChanged = new ArraySet<>(); private WindowInsets mLastInsets; private boolean mAnimCallbackScheduled; @@ -593,6 +590,7 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation private boolean mStartingAnimation; private int mCaptionInsetsHeight = 0; private boolean mAnimationsDisabled; + private boolean mCompatSysUiVisibilityStaled; private final Runnable mPendingControlTimeout = this::abortPendingImeControlRequest; private final ArrayList<OnControllableInsetsChangedListener> mControllableInsetsChangedListeners @@ -604,6 +602,18 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation /** Set of inset types which cannot be controlled by the user animation */ private @InsetsType int mDisabledUserAnimationInsetsTypes; + /** Set of inset types which are visible */ + private @InsetsType int mVisibleTypes = WindowInsets.Type.defaultVisible(); + + /** Set of inset types which are requested visible */ + private @InsetsType int mRequestedVisibleTypes = WindowInsets.Type.defaultVisible(); + + /** Set of inset types which are requested visible which are reported to the host */ + private @InsetsType int mReportedRequestedVisibleTypes = WindowInsets.Type.defaultVisible(); + + /** Set of inset types that we have controls of */ + private @InsetsType int mControllableTypes; + private final Runnable mInvokeControllableInsetsChangedListeners = this::invokeControllableInsetsChangedListeners; @@ -687,8 +697,8 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation } @Override - public boolean isRequestedVisible(int type) { - return getSourceConsumer(type).isRequestedVisible(); + public @InsetsType int getRequestedVisibleTypes() { + return mRequestedVisibleTypes; } public InsetsState getLastDispatchedState() { @@ -715,6 +725,7 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation final InsetsState lastState = new InsetsState(mState, true /* copySources */); updateState(state); applyLocalVisibilityOverride(); + updateCompatSysUiVisibility(); if (!mState.equals(lastState, false /* excludingCaptionInsets */, true /* excludeInvisibleIme */)) { @@ -727,14 +738,15 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation private void updateState(InsetsState newState) { mState.set(newState, 0 /* types */); + @InsetsType int visibleTypes = 0; @InsetsType int disabledUserAnimationTypes = 0; @InsetsType int[] cancelledUserAnimationTypes = {0}; for (@InternalInsetsType int type = 0; type < InsetsState.SIZE; type++) { InsetsSource source = newState.peekSource(type); if (source == null) continue; @AnimationType int animationType = getAnimationType(type); + @InsetsType int insetsType = toPublicType(type); if (!source.isUserControllable()) { - @InsetsType int insetsType = toPublicType(type); // The user animation is not allowed when visible frame is empty. disabledUserAnimationTypes |= insetsType; if (animationType == ANIMATION_TYPE_USER) { @@ -744,6 +756,15 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation } } getSourceConsumer(type).updateSource(source, animationType); + if (source.isVisible()) { + visibleTypes |= insetsType; + } + } + if (mVisibleTypes != visibleTypes) { + if (WindowInsets.Type.hasCompatSystemBars(mVisibleTypes ^ visibleTypes)) { + mCompatSysUiVisibilityStaled = true; + } + mVisibleTypes = visibleTypes; } for (@InternalInsetsType int type = 0; type < InsetsState.SIZE; type++) { // Only update the server side insets here. @@ -829,7 +850,8 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation } /** - * @see InsetsState#calculateInsets + * @see InsetsState#calculateInsets(Rect, InsetsState, boolean, boolean, int, int, int, int, + * int, SparseIntArray) */ @VisibleForTesting public WindowInsets calculateInsets(boolean isScreenRound, boolean alwaysConsumeSystemBars, @@ -868,7 +890,7 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation } } - boolean requestedVisibilityStale = false; + @InsetsType int controllableTypes = 0; final int[] showTypes = new int[1]; final int[] hideTypes = new int[1]; @@ -888,22 +910,7 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation final @InternalInsetsType int type = control.getType(); final InsetsSourceConsumer consumer = getSourceConsumer(type); consumer.setControl(control, showTypes, hideTypes); - - if (!requestedVisibilityStale) { - final boolean requestedVisible = consumer.isRequestedVisible(); - - // We might have changed our requested visibilities while we don't have the control, - // so we need to update our requested state once we have control. Otherwise, our - // requested state at the server side might be incorrect. - final boolean requestedVisibilityChanged = - requestedVisible != mRequestedVisibilities.getVisibility(type); - - // The IME client visibility will be reset by insets source provider while updating - // control, so if IME is requested visible, we need to send the request to server. - final boolean imeRequestedVisible = type == ITYPE_IME && requestedVisible; - - requestedVisibilityStale = requestedVisibilityChanged || imeRequestedVisible; - } + controllableTypes |= InsetsState.toPublicType(type); } if (mTmpControlArray.size() > 0) { @@ -927,8 +934,15 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation applyAnimation(hideTypes[0], false /* show */, false /* fromIme */); } + if (mControllableTypes != controllableTypes) { + if (WindowInsets.Type.hasCompatSystemBars(mControllableTypes ^ controllableTypes)) { + mCompatSysUiVisibilityStaled = true; + } + mControllableTypes = controllableTypes; + } + // InsetsSourceConsumer#setControl might change the requested visibility. - updateRequestedVisibilities(); + reportRequestedVisibleTypes(); } @Override @@ -1082,7 +1096,7 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation if (types == 0) { // nothing to animate. listener.onCancelled(null); - updateRequestedVisibilities(); + reportRequestedVisibleTypes(); if (DEBUG) Log.d(TAG, "no types to animate in controlAnimationUnchecked"); return; } @@ -1118,7 +1132,7 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation } }); } - updateRequestedVisibilities(); + reportRequestedVisibleTypes(); Trace.asyncTraceEnd(TRACE_TAG_VIEW, "IC.showRequestFromApi", 0); return; } @@ -1126,7 +1140,7 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation if (typesReady == 0) { if (DEBUG) Log.d(TAG, "No types ready. onCancelled()"); listener.onCancelled(null); - updateRequestedVisibilities(); + reportRequestedVisibleTypes(); return; } @@ -1158,7 +1172,7 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation } else { hideDirectly(types, false /* animationFinished */, animationType, fromIme); } - updateRequestedVisibilities(); + reportRequestedVisibleTypes(); } // TODO(b/242962223): Make this setter restrictive. @@ -1386,11 +1400,14 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation } /** - * @see ViewRootImpl#updateCompatSysUiVisibility(int, boolean, boolean) + * @see ViewRootImpl#updateCompatSysUiVisibility(int, int, int) */ - public void updateCompatSysUiVisibility(@InternalInsetsType int type, boolean visible, - boolean hasControl) { - mHost.updateCompatSysUiVisibility(type, visible, hasControl); + public void updateCompatSysUiVisibility() { + if (mCompatSysUiVisibilityStaled) { + mCompatSysUiVisibilityStaled = false; + mHost.updateCompatSysUiVisibility( + mVisibleTypes, mRequestedVisibleTypes, mControllableTypes); + } } /** @@ -1420,35 +1437,27 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation @VisibleForTesting public void onRequestedVisibilityChanged(InsetsSourceConsumer consumer) { - mRequestedVisibilityChanged.add(consumer); + final @InsetsType int type = InsetsState.toPublicType(consumer.getType()); + final int requestedVisibleTypes = consumer.isRequestedVisible() + ? mRequestedVisibleTypes | type + : mRequestedVisibleTypes & ~type; + if (mRequestedVisibleTypes != requestedVisibleTypes) { + mRequestedVisibleTypes = requestedVisibleTypes; + if (WindowInsets.Type.hasCompatSystemBars(type)) { + mCompatSysUiVisibilityStaled = true; + } + } } /** - * Sends the requested visibilities to window manager if any of them is changed. + * Sends the requested visible types to window manager if any of them is changed. */ - private void updateRequestedVisibilities() { - boolean changed = false; - for (int i = mRequestedVisibilityChanged.size() - 1; i >= 0; i--) { - final InsetsSourceConsumer consumer = mRequestedVisibilityChanged.valueAt(i); - final @InternalInsetsType int type = consumer.getType(); - if (type == ITYPE_CAPTION_BAR) { - continue; - } - final boolean requestedVisible = consumer.isRequestedVisible(); - if (mRequestedVisibilities.getVisibility(type) != requestedVisible) { - mRequestedVisibilities.setVisibility(type, requestedVisible); - changed = true; - } - } - mRequestedVisibilityChanged.clear(); - if (!changed) { - return; + private void reportRequestedVisibleTypes() { + updateCompatSysUiVisibility(); + if (mReportedRequestedVisibleTypes != mRequestedVisibleTypes) { + mReportedRequestedVisibleTypes = mRequestedVisibleTypes; + mHost.updateRequestedVisibleTypes(mReportedRequestedVisibleTypes); } - mHost.updateRequestedVisibilities(mRequestedVisibilities); - } - - InsetsVisibilities getRequestedVisibilities() { - return mRequestedVisibilities; } @VisibleForTesting @@ -1504,7 +1513,7 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation for (int i = internalTypes.size() - 1; i >= 0; i--) { getSourceConsumer(internalTypes.valueAt(i)).hide(animationFinished, animationType); } - updateRequestedVisibilities(); + reportRequestedVisibleTypes(); if (fromIme) { Trace.asyncTraceEnd(TRACE_TAG_VIEW, "IC.hideRequestFromIme", 0); @@ -1520,7 +1529,7 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation for (int i = internalTypes.size() - 1; i >= 0; i--) { getSourceConsumer(internalTypes.valueAt(i)).show(false /* fromIme */); } - updateRequestedVisibilities(); + reportRequestedVisibleTypes(); if (fromIme) { Trace.asyncTraceEnd(TRACE_TAG_VIEW, "IC.showRequestFromIme", 0); diff --git a/core/java/android/view/InsetsFrameProvider.java b/core/java/android/view/InsetsFrameProvider.java index 72757807169e..da54da16585d 100644 --- a/core/java/android/view/InsetsFrameProvider.java +++ b/core/java/android/view/InsetsFrameProvider.java @@ -273,6 +273,9 @@ public class InsetsFrameProvider implements Parcelable { /** * Class to describe the insets size to be provided to window with specific window type. If not * used, same insets size will be sent as instructed in the insetsSize and source. + * + * If the insetsSize of given type is set to {@code null}, the insets source frame will be used + * directly for that window type. */ public static class InsetsSizeOverride implements Parcelable { public final int windowType; @@ -280,7 +283,7 @@ public class InsetsFrameProvider implements Parcelable { protected InsetsSizeOverride(Parcel in) { windowType = in.readInt(); - insetsSize = in.readParcelable(null, android.graphics.Insets.class); + insetsSize = in.readParcelable(null, Insets.class); } public InsetsSizeOverride(int type, Insets size) { diff --git a/core/java/android/view/InsetsSourceConsumer.java b/core/java/android/view/InsetsSourceConsumer.java index 5236fe772a7b..7a498ad2358d 100644 --- a/core/java/android/view/InsetsSourceConsumer.java +++ b/core/java/android/view/InsetsSourceConsumer.java @@ -34,7 +34,6 @@ import static com.android.internal.annotations.VisibleForTesting.Visibility.PACK import android.annotation.IntDef; import android.annotation.Nullable; import android.graphics.Rect; -import android.util.ArraySet; import android.util.Log; import android.util.proto.ProtoOutputStream; import android.view.InsetsState.InternalInsetsType; @@ -149,9 +148,6 @@ public class InsetsSourceConsumer { source.setVisible(serverVisibility); mController.notifyVisibilityChanged(); } - - // For updateCompatSysUiVisibility - applyLocalVisibilityOverride(); } else { final boolean requestedVisible = isRequestedVisibleAwaitingControl(); final SurfaceControl oldLeash = lastControl != null ? lastControl.getLeash() : null; @@ -259,8 +255,6 @@ public class InsetsSourceConsumer { mController.getHost().getInputMethodManager(), null /* icProto */); } - updateCompatSysUiVisibility(hasControl, source, isVisible); - // If we don't have control, we are not able to change the visibility. if (!hasControl) { if (DEBUG) Log.d(TAG, "applyLocalVisibilityOverride: No control in " @@ -277,36 +271,6 @@ public class InsetsSourceConsumer { return true; } - private void updateCompatSysUiVisibility(boolean hasControl, InsetsSource source, - boolean visible) { - final @InsetsType int publicType = InsetsState.toPublicType(mType); - if (publicType != WindowInsets.Type.statusBars() - && publicType != WindowInsets.Type.navigationBars()) { - // System UI visibility only controls status bars and navigation bars. - return; - } - final boolean compatVisible; - if (hasControl) { - compatVisible = mRequestedVisible; - } else if (source != null && !source.getFrame().isEmpty()) { - compatVisible = visible; - } else { - final ArraySet<Integer> types = InsetsState.toInternalType(publicType); - for (int i = types.size() - 1; i >= 0; i--) { - final InsetsSource s = mState.peekSource(types.valueAt(i)); - if (s != null && !s.getFrame().isEmpty()) { - // The compat system UI visibility would be updated by another consumer which - // handles the same public insets type. - return; - } - } - // No one provides the public type. Use the requested visibility for making the callback - // behavior compatible. - compatVisible = mRequestedVisible; - } - mController.updateCompatSysUiVisibility(mType, compatVisible, hasControl); - } - @VisibleForTesting public boolean isRequestedVisible() { return mRequestedVisible; diff --git a/core/java/android/view/InsetsState.java b/core/java/android/view/InsetsState.java index 9f426a176a08..e91839baad61 100644 --- a/core/java/android/view/InsetsState.java +++ b/core/java/android/view/InsetsState.java @@ -352,7 +352,7 @@ public class InsetsState implements Parcelable { } public Insets calculateInsets(Rect frame, @InsetsType int types, - InsetsVisibilities overrideVisibilities) { + @InsetsType int requestedVisibleTypes) { Insets insets = Insets.NONE; for (int type = FIRST_TYPE; type <= LAST_TYPE; type++) { InsetsSource source = mSources[type]; @@ -360,10 +360,7 @@ public class InsetsState implements Parcelable { continue; } int publicType = InsetsState.toPublicType(type); - if ((publicType & types) == 0) { - continue; - } - if (!overrideVisibilities.getVisibility(type)) { + if ((publicType & types & requestedVisibleTypes) == 0) { continue; } insets = Insets.max(source.calculateInsets(frame, true), insets); diff --git a/core/java/android/view/MotionEvent.java b/core/java/android/view/MotionEvent.java index ceab310b64b2..a08a5a8892c0 100644 --- a/core/java/android/view/MotionEvent.java +++ b/core/java/android/view/MotionEvent.java @@ -1294,6 +1294,8 @@ public final class MotionEvent extends InputEvent implements Parcelable { // NOTE: If you add a new axis here you must also add it to: // frameworks/native/include/android/input.h // frameworks/native/libs/input/InputEventLabels.cpp + // platform/cts/tests/tests/view/src/android/view/cts/MotionEventTest.java + // (testAxisFromToString) // Symbolic names of all axes. private static final SparseArray<String> AXIS_SYMBOLIC_NAMES = new SparseArray<String>(); diff --git a/core/java/android/view/PendingInsetsController.java b/core/java/android/view/PendingInsetsController.java index 3fe9110283a6..e8f62fc0963f 100644 --- a/core/java/android/view/PendingInsetsController.java +++ b/core/java/android/view/PendingInsetsController.java @@ -45,6 +45,7 @@ public class PendingInsetsController implements WindowInsetsController { = new ArrayList<>(); private int mCaptionInsetsHeight = 0; private WindowInsetsAnimationControlListener mLoggingListener; + private @InsetsType int mRequestedVisibleTypes = WindowInsets.Type.defaultVisible(); @Override public void show(int types) { @@ -52,6 +53,7 @@ public class PendingInsetsController implements WindowInsetsController { mReplayedInsetsController.show(types); } else { mRequests.add(new ShowRequest(types)); + mRequestedVisibleTypes |= types; } } @@ -61,6 +63,7 @@ public class PendingInsetsController implements WindowInsetsController { mReplayedInsetsController.hide(types); } else { mRequests.add(new HideRequest(types)); + mRequestedVisibleTypes &= ~types; } } @@ -122,11 +125,11 @@ public class PendingInsetsController implements WindowInsetsController { } @Override - public boolean isRequestedVisible(int type) { - - // Method is only used once real insets controller is attached, so no need to traverse - // requests here. - return InsetsState.getDefaultVisibility(type); + public @InsetsType int getRequestedVisibleTypes() { + if (mReplayedInsetsController != null) { + return mReplayedInsetsController.getRequestedVisibleTypes(); + } + return mRequestedVisibleTypes; } @Override @@ -189,6 +192,7 @@ public class PendingInsetsController implements WindowInsetsController { mAppearanceMask = 0; mAnimationsDisabled = false; mLoggingListener = null; + mRequestedVisibleTypes = WindowInsets.Type.defaultVisible(); // After replaying, we forward everything directly to the replayed instance. mReplayedInsetsController = controller; } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index efda257aed27..ff4588a7bc9b 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -162,7 +162,6 @@ import android.util.TimeUtils; import android.util.TypedValue; import android.util.proto.ProtoOutputStream; import android.view.InputDevice.InputSourceClass; -import android.view.InsetsState.InternalInsetsType; import android.view.Surface.OutOfResourcesException; import android.view.SurfaceControl.Transaction; import android.view.View.AttachInfo; @@ -1245,7 +1244,7 @@ public final class ViewRootImpl implements ViewParent, final float[] sizeCompatScale = { 1f }; res = mWindowSession.addToDisplayAsUser(mWindow, mWindowAttributes, getHostVisibility(), mDisplay.getDisplayId(), userId, - mInsetsController.getRequestedVisibilities(), inputChannel, mTempInsets, + mInsetsController.getRequestedVisibleTypes(), inputChannel, mTempInsets, mTempControls, attachedFrame, sizeCompatScale); if (!attachedFrame.isValid()) { attachedFrame = null; @@ -1284,7 +1283,7 @@ public final class ViewRootImpl implements ViewParent, mWindowLayout.computeFrames(mWindowAttributes, state, displayCutoutSafe, winConfig.getBounds(), winConfig.getWindowingMode(), UNSPECIFIED_LENGTH, UNSPECIFIED_LENGTH, - mInsetsController.getRequestedVisibilities(), 1f /* compactScale */, + mInsetsController.getRequestedVisibleTypes(), 1f /* compactScale */, mTmpFrames); setFrame(mTmpFrames.frame); registerBackCallbackOnWindow(); @@ -2376,7 +2375,7 @@ public final class ViewRootImpl implements ViewParent, mCompatibleVisibilityInfo.globalVisibility = (mCompatibleVisibilityInfo.globalVisibility & ~View.SYSTEM_UI_FLAG_LOW_PROFILE) | (mAttachInfo.mSystemUiVisibility & View.SYSTEM_UI_FLAG_LOW_PROFILE); - dispatchDispatchSystemUiVisibilityChanged(mCompatibleVisibilityInfo); + dispatchDispatchSystemUiVisibilityChanged(); if (mAttachInfo.mKeepScreenOn != oldScreenOn || mAttachInfo.mSystemUiVisibility != params.subtreeSystemUiVisibility || mAttachInfo.mHasSystemUiListeners != params.hasSystemUiListeners) { @@ -2404,24 +2403,29 @@ public final class ViewRootImpl implements ViewParent, /** * Update the compatible system UI visibility for dispatching it to the legacy app. - * - * @param type Indicates which type of the insets source we are handling. - * @param visible True if the insets source is visible. - * @param hasControl True if we can control the insets source. - */ - void updateCompatSysUiVisibility(@InternalInsetsType int type, boolean visible, - boolean hasControl) { - @InsetsType final int publicType = InsetsState.toPublicType(type); - if (publicType != Type.statusBars() && publicType != Type.navigationBars()) { - return; - } + */ + void updateCompatSysUiVisibility(@InsetsType int visibleTypes, + @InsetsType int requestedVisibleTypes, @InsetsType int controllableTypes) { + // If a type is controllable, the visibility is overridden by the requested visibility. + visibleTypes = + (requestedVisibleTypes & controllableTypes) | (visibleTypes & ~controllableTypes); + + updateCompatSystemUiVisibilityInfo(SYSTEM_UI_FLAG_FULLSCREEN, Type.statusBars(), + visibleTypes, controllableTypes); + updateCompatSystemUiVisibilityInfo(SYSTEM_UI_FLAG_HIDE_NAVIGATION, Type.navigationBars(), + visibleTypes, controllableTypes); + dispatchDispatchSystemUiVisibilityChanged(); + } + + private void updateCompatSystemUiVisibilityInfo(int systemUiFlag, @InsetsType int insetsType, + @InsetsType int visibleTypes, @InsetsType int controllableTypes) { final SystemUiVisibilityInfo info = mCompatibleVisibilityInfo; - final int systemUiFlag = publicType == Type.statusBars() - ? View.SYSTEM_UI_FLAG_FULLSCREEN - : View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; - if (visible) { + final boolean willBeVisible = (visibleTypes & insetsType) != 0; + final boolean hasControl = (controllableTypes & insetsType) != 0; + final boolean wasInvisible = (mAttachInfo.mSystemUiVisibility & systemUiFlag) != 0; + if (willBeVisible) { info.globalVisibility &= ~systemUiFlag; - if (hasControl && (mAttachInfo.mSystemUiVisibility & systemUiFlag) != 0) { + if (hasControl && wasInvisible) { // The local system UI visibility can only be cleared while we have the control. info.localChanges |= systemUiFlag; } @@ -2429,7 +2433,6 @@ public final class ViewRootImpl implements ViewParent, info.globalVisibility |= systemUiFlag; info.localChanges &= ~systemUiFlag; } - dispatchDispatchSystemUiVisibilityChanged(info); } /** @@ -2445,25 +2448,28 @@ public final class ViewRootImpl implements ViewParent, && (info.globalVisibility & SYSTEM_UI_FLAG_LOW_PROFILE) != 0) { info.globalVisibility &= ~SYSTEM_UI_FLAG_LOW_PROFILE; info.localChanges |= SYSTEM_UI_FLAG_LOW_PROFILE; - dispatchDispatchSystemUiVisibilityChanged(info); + dispatchDispatchSystemUiVisibilityChanged(); } } - private void dispatchDispatchSystemUiVisibilityChanged(SystemUiVisibilityInfo args) { - if (mDispatchedSystemUiVisibility != args.globalVisibility) { + private void dispatchDispatchSystemUiVisibilityChanged() { + if (mDispatchedSystemUiVisibility != mCompatibleVisibilityInfo.globalVisibility) { mHandler.removeMessages(MSG_DISPATCH_SYSTEM_UI_VISIBILITY); - mHandler.sendMessage(mHandler.obtainMessage(MSG_DISPATCH_SYSTEM_UI_VISIBILITY, args)); + mHandler.sendMessage(mHandler.obtainMessage(MSG_DISPATCH_SYSTEM_UI_VISIBILITY)); } } - private void handleDispatchSystemUiVisibilityChanged(SystemUiVisibilityInfo args) { - if (mView == null) return; - if (args.localChanges != 0) { - mView.updateLocalSystemUiVisibility(args.localValue, args.localChanges); - args.localChanges = 0; + private void handleDispatchSystemUiVisibilityChanged() { + if (mView == null) { + return; + } + final SystemUiVisibilityInfo info = mCompatibleVisibilityInfo; + if (info.localChanges != 0) { + mView.updateLocalSystemUiVisibility(info.localValue, info.localChanges); + info.localChanges = 0; } - final int visibility = args.globalVisibility & View.SYSTEM_UI_CLEARABLE_FLAGS; + final int visibility = info.globalVisibility & View.SYSTEM_UI_CLEARABLE_FLAGS; if (mDispatchedSystemUiVisibility != visibility) { mDispatchedSystemUiVisibility = visibility; mView.dispatchSystemUiVisibilityChanged(visibility); @@ -4964,7 +4970,7 @@ public final class ViewRootImpl implements ViewParent, } void reportKeepClearAreasChanged() { - if (!mHasPendingKeepClearAreaChange) { + if (!mHasPendingKeepClearAreaChange || mView == null) { return; } mHasPendingKeepClearAreaChange = false; @@ -5728,7 +5734,7 @@ public final class ViewRootImpl implements ViewParent, handleDragEvent(event); } break; case MSG_DISPATCH_SYSTEM_UI_VISIBILITY: { - handleDispatchSystemUiVisibilityChanged((SystemUiVisibilityInfo) msg.obj); + handleDispatchSystemUiVisibilityChanged(); } break; case MSG_UPDATE_CONFIGURATION: { Configuration config = (Configuration) msg.obj; @@ -8148,7 +8154,7 @@ public final class ViewRootImpl implements ViewParent, state.getDisplayCutoutSafe(displayCutoutSafe); mWindowLayout.computeFrames(mWindowAttributes.forRotation(winConfig.getRotation()), state, displayCutoutSafe, winConfig.getBounds(), winConfig.getWindowingMode(), - measuredWidth, measuredHeight, mInsetsController.getRequestedVisibilities(), + measuredWidth, measuredHeight, mInsetsController.getRequestedVisibleTypes(), 1f /* compatScale */, mTmpFrames); mWinFrameInScreen.set(mTmpFrames.frame); if (mTranslator != null) { diff --git a/core/java/android/view/ViewRootInsetsControllerHost.java b/core/java/android/view/ViewRootInsetsControllerHost.java index d960ba1489ca..c59d83ec4c6e 100644 --- a/core/java/android/view/ViewRootInsetsControllerHost.java +++ b/core/java/android/view/ViewRootInsetsControllerHost.java @@ -145,15 +145,17 @@ public class ViewRootInsetsControllerHost implements InsetsController.Host { } @Override - public void updateCompatSysUiVisibility(int type, boolean visible, boolean hasControl) { - mViewRoot.updateCompatSysUiVisibility(type, visible, hasControl); + public void updateCompatSysUiVisibility(int visibleTypes, int requestedVisibleTypes, + int controllableTypes) { + mViewRoot.updateCompatSysUiVisibility(visibleTypes, requestedVisibleTypes, + controllableTypes); } @Override - public void updateRequestedVisibilities(InsetsVisibilities vis) { + public void updateRequestedVisibleTypes(@WindowInsets.Type.InsetsType int types) { try { if (mViewRoot.mAdded) { - mViewRoot.mWindowSession.updateRequestedVisibilities(mViewRoot.mWindow, vis); + mViewRoot.mWindowSession.updateRequestedVisibleTypes(mViewRoot.mWindow, types); } } catch (RemoteException e) { Log.e(TAG, "Failed to call insetsModified", e); diff --git a/core/java/android/view/WindowInsets.java b/core/java/android/view/WindowInsets.java index c1dddbee408f..2a76c4e0a694 100644 --- a/core/java/android/view/WindowInsets.java +++ b/core/java/android/view/WindowInsets.java @@ -1429,6 +1429,8 @@ public final class WindowInsets { static final int LAST = GENERIC_OVERLAYS; static final int SIZE = 10; + static final int DEFAULT_VISIBLE = ~IME; + static int indexOf(@InsetsType int type) { switch (type) { case STATUS_BARS: @@ -1457,7 +1459,8 @@ public final class WindowInsets { } } - static String toString(@InsetsType int types) { + /** @hide */ + public static String toString(@InsetsType int types) { StringBuilder result = new StringBuilder(); if ((types & STATUS_BARS) != 0) { result.append("statusBars |"); @@ -1598,6 +1601,15 @@ public final class WindowInsets { } /** + * @return Default visible types. + * + * @hide + */ + public static @InsetsType int defaultVisible() { + return DEFAULT_VISIBLE; + } + + /** * @return All inset types combined. * * @hide @@ -1605,6 +1617,15 @@ public final class WindowInsets { public static @InsetsType int all() { return 0xFFFFFFFF; } + + /** + * @return System bars which can be controlled by {@link View.SystemUiVisibility}. + * + * @hide + */ + public static boolean hasCompatSystemBars(@InsetsType int types) { + return (types & (STATUS_BARS | NAVIGATION_BARS)) != 0; + } } /** diff --git a/core/java/android/view/WindowInsetsController.java b/core/java/android/view/WindowInsetsController.java index 63f9e13214ff..bc0bab7b5e95 100644 --- a/core/java/android/view/WindowInsetsController.java +++ b/core/java/android/view/WindowInsetsController.java @@ -23,7 +23,6 @@ import android.graphics.Insets; import android.inputmethodservice.InputMethodService; import android.os.Build; import android.os.CancellationSignal; -import android.view.InsetsState.InternalInsetsType; import android.view.WindowInsets.Type; import android.view.WindowInsets.Type.InsetsType; import android.view.animation.Interpolator; @@ -279,11 +278,10 @@ public interface WindowInsetsController { InsetsState getState(); /** - * @return Whether the specified insets source is currently requested to be visible by the - * application. + * @return Insets types that have been requested to be visible. * @hide */ - boolean isRequestedVisible(@InternalInsetsType int type); + @InsetsType int getRequestedVisibleTypes(); /** * Adds a {@link OnControllableInsetsChangedListener} to the window insets controller. diff --git a/core/java/android/view/WindowLayout.java b/core/java/android/view/WindowLayout.java index 5ed9d2f90a72..70778046d9f5 100644 --- a/core/java/android/view/WindowLayout.java +++ b/core/java/android/view/WindowLayout.java @@ -40,6 +40,7 @@ import android.graphics.Insets; import android.graphics.Point; import android.graphics.Rect; import android.util.Log; +import android.view.WindowInsets.Type.InsetsType; import android.window.ClientWindowFrames; /** @@ -63,7 +64,7 @@ public class WindowLayout { public void computeFrames(WindowManager.LayoutParams attrs, InsetsState state, Rect displayCutoutSafe, Rect windowBounds, @WindowingMode int windowingMode, - int requestedWidth, int requestedHeight, InsetsVisibilities requestedVisibilities, + int requestedWidth, int requestedHeight, @InsetsType int requestedVisibleTypes, float compatScale, ClientWindowFrames frames) { final int type = attrs.type; final int fl = attrs.flags; @@ -130,7 +131,7 @@ public class WindowLayout { && (cutoutMode == LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT || cutoutMode == LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES)) { final Insets systemBarsInsets = state.calculateInsets( - displayFrame, WindowInsets.Type.systemBars(), requestedVisibilities); + displayFrame, WindowInsets.Type.systemBars(), requestedVisibleTypes); if (systemBarsInsets.left > 0) { displayCutoutSafeExceptMaybeBars.left = MIN_X; } @@ -288,7 +289,7 @@ public class WindowLayout { + " displayCutoutSafe=" + displayCutoutSafe + " attrs=" + attrs + " state=" + state - + " requestedVisibilities=" + requestedVisibilities); + + " requestedInvisibleTypes=" + WindowInsets.Type.toString(~requestedVisibleTypes)); } public static void extendFrameByCutout(Rect displayCutoutSafe, diff --git a/core/java/android/view/WindowlessWindowLayout.java b/core/java/android/view/WindowlessWindowLayout.java index 5bec5b6b6722..8ef4d7874f02 100644 --- a/core/java/android/view/WindowlessWindowLayout.java +++ b/core/java/android/view/WindowlessWindowLayout.java @@ -18,6 +18,7 @@ package android.view; import android.app.WindowConfiguration.WindowingMode; import android.graphics.Rect; +import android.view.WindowInsets.Type.InsetsType; import android.window.ClientWindowFrames; /** @@ -29,7 +30,7 @@ public class WindowlessWindowLayout extends WindowLayout { @Override public void computeFrames(WindowManager.LayoutParams attrs, InsetsState state, Rect displayCutoutSafe, Rect windowBounds, @WindowingMode int windowingMode, - int requestedWidth, int requestedHeight, InsetsVisibilities requestedVisibilities, + int requestedWidth, int requestedHeight, @InsetsType int requestedVisibleTypes, float compatScale, ClientWindowFrames frames) { frames.frame.set(0, 0, attrs.width, attrs.height); frames.displayFrame.set(frames.frame); diff --git a/core/java/android/view/WindowlessWindowManager.java b/core/java/android/view/WindowlessWindowManager.java index fbf7456ac05d..69340aa39daf 100644 --- a/core/java/android/view/WindowlessWindowManager.java +++ b/core/java/android/view/WindowlessWindowManager.java @@ -30,6 +30,7 @@ import android.os.RemoteCallback; import android.os.RemoteException; import android.util.Log; import android.util.MergedConfiguration; +import android.view.WindowInsets.Type.InsetsType; import android.window.ClientWindowFrames; import android.window.OnBackInvokedCallbackInfo; @@ -147,7 +148,7 @@ public class WindowlessWindowManager implements IWindowSession { */ @Override public int addToDisplay(IWindow window, WindowManager.LayoutParams attrs, - int viewVisibility, int displayId, InsetsVisibilities requestedVisibilities, + int viewVisibility, int displayId, @InsetsType int requestedVisibleTypes, InputChannel outInputChannel, InsetsState outInsetsState, InsetsSourceControl[] outActiveControls, Rect outAttachedFrame, float[] outSizeCompatScale) { @@ -198,11 +199,11 @@ public class WindowlessWindowManager implements IWindowSession { */ @Override public int addToDisplayAsUser(IWindow window, WindowManager.LayoutParams attrs, - int viewVisibility, int displayId, int userId, InsetsVisibilities requestedVisibilities, + int viewVisibility, int displayId, int userId, @InsetsType int requestedVisibleTypes, InputChannel outInputChannel, InsetsState outInsetsState, InsetsSourceControl[] outActiveControls, Rect outAttachedFrame, float[] outSizeCompatScale) { - return addToDisplay(window, attrs, viewVisibility, displayId, requestedVisibilities, + return addToDisplay(window, attrs, viewVisibility, displayId, requestedVisibleTypes, outInputChannel, outInsetsState, outActiveControls, outAttachedFrame, outSizeCompatScale); } @@ -491,7 +492,8 @@ public class WindowlessWindowManager implements IWindowSession { } @Override - public void updateRequestedVisibilities(IWindow window, InsetsVisibilities visibilities) { + public void updateRequestedVisibleTypes(IWindow window, + @InsetsType int requestedVisibleTypes) { } @Override diff --git a/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java new file mode 100644 index 000000000000..aeff37c67444 --- /dev/null +++ b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java @@ -0,0 +1,515 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view.inputmethod; + +import android.annotation.AnyThread; +import android.annotation.DurationMillisLong; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.RequiresNoPermission; +import android.annotation.RequiresPermission; +import android.annotation.UserIdInt; +import android.content.Context; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.ResultReceiver; +import android.os.ServiceManager; +import android.view.WindowManager; +import android.window.ImeOnBackInvokedDispatcher; + +import com.android.internal.inputmethod.DirectBootAwareness; +import com.android.internal.inputmethod.IInputMethodClient; +import com.android.internal.inputmethod.IRemoteAccessibilityInputConnection; +import com.android.internal.inputmethod.IRemoteInputConnection; +import com.android.internal.inputmethod.InputBindResult; +import com.android.internal.inputmethod.SoftInputShowHideReason; +import com.android.internal.inputmethod.StartInputFlags; +import com.android.internal.inputmethod.StartInputReason; +import com.android.internal.view.IInputMethodManager; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +/** + * A global wrapper to directly invoke {@link IInputMethodManager} IPCs. + * + * <p>All public static methods are guaranteed to be thread-safe.</p> + * + * <p>All public methods are guaranteed to do nothing when {@link IInputMethodManager} is + * unavailable.</p> + * + * <p>If you want to use any of this method outside of {@code android.view.inputmethod}, create + * a wrapper method in {@link InputMethodManagerGlobal} instead of making this class public.</p> + */ +final class IInputMethodManagerGlobalInvoker { + @Nullable + private static volatile IInputMethodManager sServiceCache = null; + + /** + * @return {@code true} if {@link IInputMethodManager} is available. + */ + @AnyThread + static boolean isAvailable() { + return getService() != null; + } + + @AnyThread + @Nullable + static IInputMethodManager getService() { + IInputMethodManager service = sServiceCache; + if (service == null) { + if (InputMethodManager.isInEditModeInternal()) { + return null; + } + service = IInputMethodManager.Stub.asInterface( + ServiceManager.getService(Context.INPUT_METHOD_SERVICE)); + if (service == null) { + return null; + } + sServiceCache = service; + } + return service; + } + + @AnyThread + private static void handleRemoteExceptionOrRethrow(@NonNull RemoteException e, + @Nullable Consumer<RemoteException> exceptionHandler) { + if (exceptionHandler != null) { + exceptionHandler.accept(e); + } else { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Invokes {@link IInputMethodManager#startProtoDump(byte[], int, String)}. + * + * @param protoDump client or service side information to be stored by the server + * @param source where the information is coming from, refer to + * {@link com.android.internal.inputmethod.ImeTracing#IME_TRACING_FROM_CLIENT} and + * {@link com.android.internal.inputmethod.ImeTracing#IME_TRACING_FROM_IMS} + * @param where where the information is coming from. + * @param exceptionHandler an optional {@link RemoteException} handler. + */ + @RequiresNoPermission + @AnyThread + static void startProtoDump(byte[] protoDump, int source, String where, + @Nullable Consumer<RemoteException> exceptionHandler) { + final IInputMethodManager service = getService(); + if (service == null) { + return; + } + try { + service.startProtoDump(protoDump, source, where); + } catch (RemoteException e) { + handleRemoteExceptionOrRethrow(e, exceptionHandler); + } + } + + /** + * Invokes {@link IInputMethodManager#startImeTrace()}. + * + * @param exceptionHandler an optional {@link RemoteException} handler. + */ + @RequiresPermission(android.Manifest.permission.CONTROL_UI_TRACING) + @AnyThread + static void startImeTrace(@Nullable Consumer<RemoteException> exceptionHandler) { + final IInputMethodManager service = getService(); + if (service == null) { + return; + } + try { + service.startImeTrace(); + } catch (RemoteException e) { + handleRemoteExceptionOrRethrow(e, exceptionHandler); + } + } + + /** + * Invokes {@link IInputMethodManager#stopImeTrace()}. + * + * @param exceptionHandler an optional {@link RemoteException} handler. + */ + @RequiresPermission(android.Manifest.permission.CONTROL_UI_TRACING) + @AnyThread + static void stopImeTrace(@Nullable Consumer<RemoteException> exceptionHandler) { + final IInputMethodManager service = getService(); + if (service == null) { + return; + } + try { + service.stopImeTrace(); + } catch (RemoteException e) { + handleRemoteExceptionOrRethrow(e, exceptionHandler); + } + } + + /** + * Invokes {@link IInputMethodManager#isImeTraceEnabled()}. + * + * @return The return value of {@link IInputMethodManager#isImeTraceEnabled()}. + */ + @RequiresNoPermission + @AnyThread + static boolean isImeTraceEnabled() { + final IInputMethodManager service = getService(); + if (service == null) { + return false; + } + try { + return service.isImeTraceEnabled(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Invokes {@link IInputMethodManager#removeImeSurface()} + */ + @RequiresPermission(android.Manifest.permission.INTERNAL_SYSTEM_WINDOW) + @AnyThread + static void removeImeSurface(@Nullable Consumer<RemoteException> exceptionHandler) { + final IInputMethodManager service = getService(); + if (service == null) { + return; + } + try { + service.removeImeSurface(); + } catch (RemoteException e) { + handleRemoteExceptionOrRethrow(e, exceptionHandler); + } + } + + @AnyThread + static void addClient(@NonNull IInputMethodClient client, + @NonNull IRemoteInputConnection fallbackInputConnection, int untrustedDisplayId) { + final IInputMethodManager service = getService(); + if (service == null) { + return; + } + try { + service.addClient(client, fallbackInputConnection, untrustedDisplayId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + @AnyThread + @NonNull + static List<InputMethodInfo> getInputMethodList(@UserIdInt int userId, + @DirectBootAwareness int directBootAwareness) { + final IInputMethodManager service = getService(); + if (service == null) { + return new ArrayList<>(); + } + try { + return service.getInputMethodList(userId, directBootAwareness); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + @AnyThread + @NonNull + static List<InputMethodInfo> getEnabledInputMethodList(@UserIdInt int userId) { + final IInputMethodManager service = getService(); + if (service == null) { + return new ArrayList<>(); + } + try { + return service.getEnabledInputMethodList(userId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + @AnyThread + @NonNull + static List<InputMethodSubtype> getEnabledInputMethodSubtypeList(@Nullable String imiId, + boolean allowsImplicitlyEnabledSubtypes, @UserIdInt int userId) { + final IInputMethodManager service = getService(); + if (service == null) { + return new ArrayList<>(); + } + try { + return service.getEnabledInputMethodSubtypeList(imiId, + allowsImplicitlyEnabledSubtypes, userId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + @AnyThread + @Nullable + static InputMethodSubtype getLastInputMethodSubtype(@UserIdInt int userId) { + final IInputMethodManager service = getService(); + if (service == null) { + return null; + } + try { + return service.getLastInputMethodSubtype(userId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + @AnyThread + static boolean showSoftInput(@NonNull IInputMethodClient client, @Nullable IBinder windowToken, + int flags, int lastClickToolType, @Nullable ResultReceiver resultReceiver, + @SoftInputShowHideReason int reason) { + final IInputMethodManager service = getService(); + if (service == null) { + return false; + } + try { + return service.showSoftInput( + client, windowToken, flags, lastClickToolType, resultReceiver, reason); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + @AnyThread + static boolean hideSoftInput(@NonNull IInputMethodClient client, @Nullable IBinder windowToken, + int flags, @Nullable ResultReceiver resultReceiver, + @SoftInputShowHideReason int reason) { + final IInputMethodManager service = getService(); + if (service == null) { + return false; + } + try { + return service.hideSoftInput(client, windowToken, flags, resultReceiver, reason); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + @AnyThread + @NonNull + static InputBindResult startInputOrWindowGainedFocus(@StartInputReason int startInputReason, + @NonNull IInputMethodClient client, @Nullable IBinder windowToken, + @StartInputFlags int startInputFlags, + @WindowManager.LayoutParams.SoftInputModeFlags int softInputMode, + @WindowManager.LayoutParams.Flags int windowFlags, @Nullable EditorInfo editorInfo, + @Nullable IRemoteInputConnection remoteInputConnection, + @Nullable IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection, + int unverifiedTargetSdkVersion, @UserIdInt int userId, + @NonNull ImeOnBackInvokedDispatcher imeDispatcher) { + final IInputMethodManager service = getService(); + if (service == null) { + return InputBindResult.NULL; + } + try { + return service.startInputOrWindowGainedFocus(startInputReason, client, windowToken, + startInputFlags, softInputMode, windowFlags, editorInfo, remoteInputConnection, + remoteAccessibilityInputConnection, unverifiedTargetSdkVersion, userId, + imeDispatcher); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + @AnyThread + static void showInputMethodPickerFromClient(@NonNull IInputMethodClient client, + int auxiliarySubtypeMode) { + final IInputMethodManager service = getService(); + if (service == null) { + return; + } + try { + service.showInputMethodPickerFromClient(client, auxiliarySubtypeMode); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + @AnyThread + static void showInputMethodPickerFromSystem(@NonNull IInputMethodClient client, + int auxiliarySubtypeMode, int displayId) { + final IInputMethodManager service = getService(); + if (service == null) { + return; + } + try { + service.showInputMethodPickerFromSystem(client, auxiliarySubtypeMode, displayId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + @AnyThread + static boolean isInputMethodPickerShownForTest() { + final IInputMethodManager service = getService(); + if (service == null) { + return false; + } + try { + return service.isInputMethodPickerShownForTest(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + @AnyThread + @Nullable + static InputMethodSubtype getCurrentInputMethodSubtype(@UserIdInt int userId) { + final IInputMethodManager service = getService(); + if (service == null) { + return null; + } + try { + return service.getCurrentInputMethodSubtype(userId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + @AnyThread + static void setAdditionalInputMethodSubtypes(@NonNull String imeId, + @NonNull InputMethodSubtype[] subtypes, @UserIdInt int userId) { + final IInputMethodManager service = getService(); + if (service == null) { + return; + } + try { + service.setAdditionalInputMethodSubtypes(imeId, subtypes, userId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + @AnyThread + static void setExplicitlyEnabledInputMethodSubtypes(@NonNull String imeId, + @NonNull int[] subtypeHashCodes, @UserIdInt int userId) { + final IInputMethodManager service = getService(); + if (service == null) { + return; + } + try { + service.setExplicitlyEnabledInputMethodSubtypes(imeId, subtypeHashCodes, userId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + @AnyThread + static int getInputMethodWindowVisibleHeight(@NonNull IInputMethodClient client) { + final IInputMethodManager service = getService(); + if (service == null) { + return 0; + } + try { + return service.getInputMethodWindowVisibleHeight(client); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + @AnyThread + static void reportVirtualDisplayGeometryAsync(@NonNull IInputMethodClient client, + int childDisplayId, @Nullable float[] matrixValues) { + final IInputMethodManager service = getService(); + if (service == null) { + return; + } + try { + service.reportVirtualDisplayGeometryAsync(client, childDisplayId, matrixValues); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + @AnyThread + static void reportPerceptibleAsync(@NonNull IBinder windowToken, boolean perceptible) { + final IInputMethodManager service = getService(); + if (service == null) { + return; + } + try { + service.reportPerceptibleAsync(windowToken, perceptible); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + @AnyThread + static void removeImeSurfaceFromWindowAsync(@NonNull IBinder windowToken) { + final IInputMethodManager service = getService(); + if (service == null) { + return; + } + try { + service.removeImeSurfaceFromWindowAsync(windowToken); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + @AnyThread + static void startStylusHandwriting(@NonNull IInputMethodClient client) { + final IInputMethodManager service = getService(); + if (service == null) { + return; + } + try { + service.startStylusHandwriting(client); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + @AnyThread + static boolean isStylusHandwritingAvailableAsUser(@UserIdInt int userId) { + final IInputMethodManager service = getService(); + if (service == null) { + return false; + } + try { + return service.isStylusHandwritingAvailableAsUser(userId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + @AnyThread + static void addVirtualStylusIdForTestSession(IInputMethodClient client) { + final IInputMethodManager service = getService(); + if (service == null) { + return; + } + try { + service.addVirtualStylusIdForTestSession(client); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + @AnyThread + static void setStylusWindowIdleTimeoutForTest( + IInputMethodClient client, @DurationMillisLong long timeout) { + final IInputMethodManager service = getService(); + if (service == null) { + return; + } + try { + service.setStylusWindowIdleTimeoutForTest(client, timeout); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } +} diff --git a/core/java/android/view/inputmethod/IInputMethodManagerInvoker.java b/core/java/android/view/inputmethod/IInputMethodManagerInvoker.java deleted file mode 100644 index 01e8b3412614..000000000000 --- a/core/java/android/view/inputmethod/IInputMethodManagerInvoker.java +++ /dev/null @@ -1,288 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.view.inputmethod; - -import android.annotation.AnyThread; -import android.annotation.DurationMillisLong; -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.annotation.UserIdInt; -import android.os.IBinder; -import android.os.RemoteException; -import android.os.ResultReceiver; -import android.view.WindowManager; -import android.view.WindowManager.LayoutParams.SoftInputModeFlags; -import android.window.ImeOnBackInvokedDispatcher; - -import com.android.internal.inputmethod.DirectBootAwareness; -import com.android.internal.inputmethod.IInputMethodClient; -import com.android.internal.inputmethod.IRemoteAccessibilityInputConnection; -import com.android.internal.inputmethod.IRemoteInputConnection; -import com.android.internal.inputmethod.InputBindResult; -import com.android.internal.inputmethod.SoftInputShowHideReason; -import com.android.internal.inputmethod.StartInputFlags; -import com.android.internal.inputmethod.StartInputReason; -import com.android.internal.view.IInputMethodManager; - -import java.util.List; - -/** - * A wrapper class to invoke IPCs defined in {@link IInputMethodManager}. - */ -final class IInputMethodManagerInvoker { - @NonNull - private final IInputMethodManager mTarget; - - private IInputMethodManagerInvoker(@NonNull IInputMethodManager target) { - mTarget = target; - } - - @AnyThread - @NonNull - static IInputMethodManagerInvoker create(@NonNull IInputMethodManager imm) { - return new IInputMethodManagerInvoker(imm); - } - - @AnyThread - void addClient(@NonNull IInputMethodClient client, - @NonNull IRemoteInputConnection fallbackInputConnection, int untrustedDisplayId) { - try { - mTarget.addClient(client, fallbackInputConnection, untrustedDisplayId); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } - - @AnyThread - @NonNull - List<InputMethodInfo> getInputMethodList(@UserIdInt int userId, - @DirectBootAwareness int directBootAwareness) { - try { - return mTarget.getInputMethodList(userId, directBootAwareness); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } - - @AnyThread - @NonNull - List<InputMethodInfo> getEnabledInputMethodList(@UserIdInt int userId) { - try { - return mTarget.getEnabledInputMethodList(userId); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } - - @AnyThread - @NonNull - List<InputMethodSubtype> getEnabledInputMethodSubtypeList(@Nullable String imiId, - boolean allowsImplicitlyEnabledSubtypes, @UserIdInt int userId) { - try { - return mTarget.getEnabledInputMethodSubtypeList(imiId, - allowsImplicitlyEnabledSubtypes, userId); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } - - @AnyThread - @Nullable - InputMethodSubtype getLastInputMethodSubtype(@UserIdInt int userId) { - try { - return mTarget.getLastInputMethodSubtype(userId); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } - - @AnyThread - boolean showSoftInput(@NonNull IInputMethodClient client, @Nullable IBinder windowToken, - int flags, int lastClickToolType, @Nullable ResultReceiver resultReceiver, - @SoftInputShowHideReason int reason) { - try { - return mTarget.showSoftInput( - client, windowToken, flags, lastClickToolType, resultReceiver, reason); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } - - @AnyThread - boolean hideSoftInput(@NonNull IInputMethodClient client, @Nullable IBinder windowToken, - int flags, @Nullable ResultReceiver resultReceiver, - @SoftInputShowHideReason int reason) { - try { - return mTarget.hideSoftInput(client, windowToken, flags, resultReceiver, reason); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } - - @AnyThread - @NonNull - InputBindResult startInputOrWindowGainedFocus(@StartInputReason int startInputReason, - @NonNull IInputMethodClient client, @Nullable IBinder windowToken, - @StartInputFlags int startInputFlags, @SoftInputModeFlags int softInputMode, - @WindowManager.LayoutParams.Flags int windowFlags, @Nullable EditorInfo editorInfo, - @Nullable IRemoteInputConnection remoteInputConnection, - @Nullable IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection, - int unverifiedTargetSdkVersion, @UserIdInt int userId, - @NonNull ImeOnBackInvokedDispatcher imeDispatcher) { - try { - return mTarget.startInputOrWindowGainedFocus(startInputReason, client, windowToken, - startInputFlags, softInputMode, windowFlags, editorInfo, remoteInputConnection, - remoteAccessibilityInputConnection, unverifiedTargetSdkVersion, userId, - imeDispatcher); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } - - @AnyThread - void showInputMethodPickerFromClient(@NonNull IInputMethodClient client, - int auxiliarySubtypeMode) { - try { - mTarget.showInputMethodPickerFromClient(client, auxiliarySubtypeMode); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } - - @AnyThread - void showInputMethodPickerFromSystem(@NonNull IInputMethodClient client, - int auxiliarySubtypeMode, int displayId) { - try { - mTarget.showInputMethodPickerFromSystem(client, auxiliarySubtypeMode, displayId); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } - - @AnyThread - boolean isInputMethodPickerShownForTest() { - try { - return mTarget.isInputMethodPickerShownForTest(); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } - - @AnyThread - @Nullable - InputMethodSubtype getCurrentInputMethodSubtype(@UserIdInt int userId) { - try { - return mTarget.getCurrentInputMethodSubtype(userId); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } - - @AnyThread - void setAdditionalInputMethodSubtypes(@NonNull String imeId, - @NonNull InputMethodSubtype[] subtypes, @UserIdInt int userId) { - try { - mTarget.setAdditionalInputMethodSubtypes(imeId, subtypes, userId); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } - - @AnyThread - void setExplicitlyEnabledInputMethodSubtypes(@NonNull String imeId, - @NonNull int[] subtypeHashCodes, @UserIdInt int userId) { - try { - mTarget.setExplicitlyEnabledInputMethodSubtypes(imeId, subtypeHashCodes, userId); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } - - @AnyThread - int getInputMethodWindowVisibleHeight(@NonNull IInputMethodClient client) { - try { - return mTarget.getInputMethodWindowVisibleHeight(client); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } - - @AnyThread - void reportVirtualDisplayGeometryAsync(@NonNull IInputMethodClient client, int childDisplayId, - @Nullable float[] matrixValues) { - try { - mTarget.reportVirtualDisplayGeometryAsync(client, childDisplayId, matrixValues); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } - - @AnyThread - void reportPerceptibleAsync(@NonNull IBinder windowToken, boolean perceptible) { - try { - mTarget.reportPerceptibleAsync(windowToken, perceptible); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } - - @AnyThread - void removeImeSurfaceFromWindowAsync(@NonNull IBinder windowToken) { - try { - mTarget.removeImeSurfaceFromWindowAsync(windowToken); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } - - @AnyThread - void startStylusHandwriting(@NonNull IInputMethodClient client) { - try { - mTarget.startStylusHandwriting(client); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } - - @AnyThread - boolean isStylusHandwritingAvailableAsUser(@UserIdInt int userId) { - try { - return mTarget.isStylusHandwritingAvailableAsUser(userId); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } - - @AnyThread - void addVirtualStylusIdForTestSession(IInputMethodClient client) { - try { - mTarget.addVirtualStylusIdForTestSession(client); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } - - @AnyThread - void setStylusWindowIdleTimeoutForTest( - IInputMethodClient client, @DurationMillisLong long timeout) { - try { - mTarget.setStylusWindowIdleTimeoutForTest(client, timeout); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } -} diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index 69eed0abb1a6..53d77e146d89 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -70,8 +70,6 @@ import android.os.Looper; import android.os.Message; import android.os.Process; import android.os.ResultReceiver; -import android.os.ServiceManager; -import android.os.ServiceManager.ServiceNotFoundException; import android.os.SystemProperties; import android.os.Trace; import android.os.UserHandle; @@ -295,7 +293,7 @@ public final class InputMethodManager { private static final String SUBTYPE_MODE_VOICE = "voice"; /** - * Provide this to {@link IInputMethodManagerInvoker#startInputOrWindowGainedFocus(int, + * Provide this to {@link IInputMethodManagerGlobalInvoker#startInputOrWindowGainedFocus(int, * IInputMethodClient, IBinder, int, int, int, EditorInfo, * com.android.internal.inputmethod.IRemoteInputConnection, IRemoteAccessibilityInputConnection, * int, int, ImeOnBackInvokedDispatcher)} to receive @@ -422,16 +420,13 @@ public final class InputMethodManager { SystemProperties.getBoolean("debug.imm.optimize_noneditable_views", true); /** - * @deprecated Use {@link #mServiceInvoker} instead. + * @deprecated Use {@link IInputMethodManagerGlobalInvoker} instead. */ @Deprecated @UnsupportedAppUsage final IInputMethodManager mService; private final Looper mMainLooper; - @NonNull - private final IInputMethodManagerInvoker mServiceInvoker; - // For scheduling work on the main thread. This also serves as our // global lock. // Remark on @UnsupportedAppUsage: there were context leaks on old versions @@ -735,7 +730,7 @@ public final class InputMethodManager { * @hide */ public void reportPerceptible(@NonNull IBinder windowToken, boolean perceptible) { - mServiceInvoker.reportPerceptibleAsync(windowToken, perceptible); + IInputMethodManagerGlobalInvoker.reportPerceptibleAsync(windowToken, perceptible); } private final class DelegateImpl implements @@ -808,7 +803,7 @@ public final class InputMethodManager { } // ignore the result - mServiceInvoker.startInputOrWindowGainedFocus( + IInputMethodManagerGlobalInvoker.startInputOrWindowGainedFocus( StartInputReason.WINDOW_FOCUS_GAIN_REPORT_ONLY, mClient, viewForWindowFocus.getWindowToken(), startInputFlags, softInputMode, windowFlags, @@ -1353,6 +1348,10 @@ public final class InputMethodManager { return false; } + static boolean isInEditModeInternal() { + return isInEditMode(); + } + @NonNull private static InputMethodManager createInstance(int displayId, Looper looper) { return isInEditMode() ? createStubInstance(displayId, looper) @@ -1361,12 +1360,9 @@ public final class InputMethodManager { @NonNull private static InputMethodManager createRealInstance(int displayId, Looper looper) { - final IInputMethodManager service; - try { - service = IInputMethodManager.Stub.asInterface( - ServiceManager.getServiceOrThrow(Context.INPUT_METHOD_SERVICE)); - } catch (ServiceNotFoundException e) { - throw new IllegalStateException(e); + final IInputMethodManager service = IInputMethodManagerGlobalInvoker.getService(); + if (service == null) { + throw new IllegalStateException("IInputMethodManager is not available"); } final InputMethodManager imm = new InputMethodManager(service, displayId, looper); // InputMethodManagerService#addClient() relies on Binder.getCalling{Pid, Uid}() to @@ -1378,7 +1374,8 @@ public final class InputMethodManager { // 1) doing so has no effect for A and 2) doing so is sufficient for B. final long identity = Binder.clearCallingIdentity(); try { - imm.mServiceInvoker.addClient(imm.mClient, imm.mFallbackInputConnection, displayId); + IInputMethodManagerGlobalInvoker.addClient(imm.mClient, imm.mFallbackInputConnection, + displayId); } finally { Binder.restoreCallingIdentity(identity); } @@ -1418,7 +1415,6 @@ public final class InputMethodManager { private InputMethodManager(@NonNull IInputMethodManager service, int displayId, Looper looper) { mService = service; // For @UnsupportedAppUsage - mServiceInvoker = IInputMethodManagerInvoker.create(service); mMainLooper = looper; mH = new H(looper); mDisplayId = displayId; @@ -1512,7 +1508,8 @@ public final class InputMethodManager { // We intentionally do not use UserHandle.getCallingUserId() here because for system // services InputMethodManagerInternal.getInputMethodListAsUser() should be used // instead. - return mServiceInvoker.getInputMethodList(UserHandle.myUserId(), DirectBootAwareness.AUTO); + return IInputMethodManagerGlobalInvoker.getInputMethodList(UserHandle.myUserId(), + DirectBootAwareness.AUTO); } /** @@ -1546,7 +1543,7 @@ public final class InputMethodManager { } return false; } - return mServiceInvoker.isStylusHandwritingAvailableAsUser(userId); + return IInputMethodManagerGlobalInvoker.isStylusHandwritingAvailableAsUser(userId); } /** @@ -1560,7 +1557,8 @@ public final class InputMethodManager { @RequiresPermission(INTERACT_ACROSS_USERS_FULL) @NonNull public List<InputMethodInfo> getInputMethodListAsUser(@UserIdInt int userId) { - return mServiceInvoker.getInputMethodList(userId, DirectBootAwareness.AUTO); + return IInputMethodManagerGlobalInvoker.getInputMethodList(userId, + DirectBootAwareness.AUTO); } /** @@ -1576,7 +1574,7 @@ public final class InputMethodManager { @NonNull public List<InputMethodInfo> getInputMethodListAsUser(@UserIdInt int userId, @DirectBootAwareness int directBootAwareness) { - return mServiceInvoker.getInputMethodList(userId, directBootAwareness); + return IInputMethodManagerGlobalInvoker.getInputMethodList(userId, directBootAwareness); } /** @@ -1591,7 +1589,7 @@ public final class InputMethodManager { // We intentionally do not use UserHandle.getCallingUserId() here because for system // services InputMethodManagerInternal.getEnabledInputMethodListAsUser() should be used // instead. - return mServiceInvoker.getEnabledInputMethodList(UserHandle.myUserId()); + return IInputMethodManagerGlobalInvoker.getEnabledInputMethodList(UserHandle.myUserId()); } /** @@ -1603,7 +1601,7 @@ public final class InputMethodManager { */ @RequiresPermission(INTERACT_ACROSS_USERS_FULL) public List<InputMethodInfo> getEnabledInputMethodListAsUser(@UserIdInt int userId) { - return mServiceInvoker.getEnabledInputMethodList(userId); + return IInputMethodManagerGlobalInvoker.getEnabledInputMethodList(userId); } /** @@ -1620,7 +1618,7 @@ public final class InputMethodManager { @NonNull public List<InputMethodSubtype> getEnabledInputMethodSubtypeList(@Nullable InputMethodInfo imi, boolean allowsImplicitlyEnabledSubtypes) { - return mServiceInvoker.getEnabledInputMethodSubtypeList( + return IInputMethodManagerGlobalInvoker.getEnabledInputMethodSubtypeList( imi == null ? null : imi.getId(), allowsImplicitlyEnabledSubtypes, UserHandle.myUserId()); @@ -2003,7 +2001,7 @@ public final class InputMethodManager { mH.executeOrSendMessage(Message.obtain(mH, MSG_ON_SHOW_REQUESTED)); Log.d(TAG, "showSoftInput() view=" + view + " flags=" + flags + " reason=" + InputMethodDebug.softInputDisplayReasonToString(reason)); - return mServiceInvoker.showSoftInput( + return IInputMethodManagerGlobalInvoker.showSoftInput( mClient, view.getWindowToken(), flags, @@ -2035,7 +2033,7 @@ public final class InputMethodManager { // Makes sure to call ImeInsetsSourceConsumer#onShowRequested on the UI thread. // TODO(b/229426865): call WindowInsetsController#show instead. mH.executeOrSendMessage(Message.obtain(mH, MSG_ON_SHOW_REQUESTED)); - mServiceInvoker.showSoftInput( + IInputMethodManagerGlobalInvoker.showSoftInput( mClient, mCurRootView.getView().getWindowToken(), flags, @@ -2116,8 +2114,8 @@ public final class InputMethodManager { return false; } - return mServiceInvoker.hideSoftInput(mClient, windowToken, flags, resultReceiver, - reason); + return IInputMethodManagerGlobalInvoker.hideSoftInput(mClient, windowToken, flags, + resultReceiver, reason); } } @@ -2165,7 +2163,7 @@ public final class InputMethodManager { return; } - mServiceInvoker.startStylusHandwriting(mClient); + IInputMethodManagerGlobalInvoker.startStylusHandwriting(mClient); // TODO(b/210039666): do we need any extra work for supporting non-native // UI toolkits? } @@ -2498,7 +2496,7 @@ public final class InputMethodManager { } final int targetUserId = editorInfo.targetInputMethodUser != null ? editorInfo.targetInputMethodUser.getIdentifier() : UserHandle.myUserId(); - res = mServiceInvoker.startInputOrWindowGainedFocus( + res = IInputMethodManagerGlobalInvoker.startInputOrWindowGainedFocus( startInputReason, mClient, windowGainingFocus, startInputFlags, softInputMode, windowFlags, editorInfo, servedInputConnection, servedInputConnection == null ? null @@ -2594,7 +2592,7 @@ public final class InputMethodManager { @TestApi public void addVirtualStylusIdForTestSession() { synchronized (mH) { - mServiceInvoker.addVirtualStylusIdForTestSession(mClient); + IInputMethodManagerGlobalInvoker.addVirtualStylusIdForTestSession(mClient); } } @@ -2608,7 +2606,7 @@ public final class InputMethodManager { @TestApi public void setStylusWindowIdleTimeoutForTest(@DurationMillisLong long timeout) { synchronized (mH) { - mServiceInvoker.setStylusWindowIdleTimeoutForTest(mClient, timeout); + IInputMethodManagerGlobalInvoker.setStylusWindowIdleTimeoutForTest(mClient, timeout); } } @@ -2745,7 +2743,7 @@ public final class InputMethodManager { Log.w(TAG, "No current root view, ignoring closeCurrentInput()"); return; } - mServiceInvoker.hideSoftInput( + IInputMethodManagerGlobalInvoker.hideSoftInput( mClient, mCurRootView.getView().getWindowToken(), HIDE_NOT_ALWAYS, @@ -2821,7 +2819,7 @@ public final class InputMethodManager { synchronized (mH) { if (isImeSessionAvailableLocked() && mCurRootView != null && mCurRootView.getWindowToken() == windowToken) { - mServiceInvoker.hideSoftInput(mClient, windowToken, 0 /* flags */, + IInputMethodManagerGlobalInvoker.hideSoftInput(mClient, windowToken, 0 /* flags */, null /* resultReceiver */, SoftInputShowHideReason.HIDE_SOFT_INPUT_BY_INSETS_API); } @@ -2835,7 +2833,7 @@ public final class InputMethodManager { */ public void removeImeSurface(@NonNull IBinder windowToken) { synchronized (mH) { - mServiceInvoker.removeImeSurfaceFromWindowAsync(windowToken); + IInputMethodManagerGlobalInvoker.removeImeSurfaceFromWindowAsync(windowToken); } } @@ -3440,12 +3438,13 @@ public final class InputMethodManager { final int mode = showAuxiliarySubtypes ? SHOW_IM_PICKER_MODE_INCLUDE_AUXILIARY_SUBTYPES : SHOW_IM_PICKER_MODE_EXCLUDE_AUXILIARY_SUBTYPES; - mServiceInvoker.showInputMethodPickerFromSystem(mClient, mode, displayId); + IInputMethodManagerGlobalInvoker.showInputMethodPickerFromSystem(mClient, mode, displayId); } @GuardedBy("mH") private void showInputMethodPickerLocked() { - mServiceInvoker.showInputMethodPickerFromClient(mClient, SHOW_IM_PICKER_MODE_AUTO); + IInputMethodManagerGlobalInvoker.showInputMethodPickerFromClient(mClient, + SHOW_IM_PICKER_MODE_AUTO); } /** @@ -3461,7 +3460,7 @@ public final class InputMethodManager { */ @TestApi public boolean isInputMethodPickerShown() { - return mServiceInvoker.isInputMethodPickerShownForTest(); + return IInputMethodManagerGlobalInvoker.isInputMethodPickerShownForTest(); } /** @@ -3500,7 +3499,7 @@ public final class InputMethodManager { */ @Nullable public InputMethodSubtype getCurrentInputMethodSubtype() { - return mServiceInvoker.getCurrentInputMethodSubtype(UserHandle.myUserId()); + return IInputMethodManagerGlobalInvoker.getCurrentInputMethodSubtype(UserHandle.myUserId()); } /** @@ -3545,7 +3544,7 @@ public final class InputMethodManager { return false; } final List<InputMethodSubtype> enabledSubtypes = - mServiceInvoker.getEnabledInputMethodSubtypeList(imeId, true, + IInputMethodManagerGlobalInvoker.getEnabledInputMethodSubtypeList(imeId, true, UserHandle.myUserId()); final int numSubtypes = enabledSubtypes.size(); for (int i = 0; i < numSubtypes; ++i) { @@ -3611,7 +3610,7 @@ public final class InputMethodManager { @UnsupportedAppUsage(trackingBug = 204906124, maxTargetSdk = Build.VERSION_CODES.TIRAMISU, publicAlternatives = "Use {@link android.view.WindowInsets} instead") public int getInputMethodWindowVisibleHeight() { - return mServiceInvoker.getInputMethodWindowVisibleHeight(mClient); + return IInputMethodManagerGlobalInvoker.getInputMethodWindowVisibleHeight(mClient); } /** @@ -3631,7 +3630,8 @@ public final class InputMethodManager { matrixValues = new float[9]; matrix.getValues(matrixValues); } - mServiceInvoker.reportVirtualDisplayGeometryAsync(mClient, childDisplayId, matrixValues); + IInputMethodManagerGlobalInvoker.reportVirtualDisplayGeometryAsync(mClient, childDisplayId, + matrixValues); } /** @@ -3738,7 +3738,8 @@ public final class InputMethodManager { @Deprecated public void setAdditionalInputMethodSubtypes(@NonNull String imiId, @NonNull InputMethodSubtype[] subtypes) { - mServiceInvoker.setAdditionalInputMethodSubtypes(imiId, subtypes, UserHandle.myUserId()); + IInputMethodManagerGlobalInvoker.setAdditionalInputMethodSubtypes(imiId, subtypes, + UserHandle.myUserId()); } /** @@ -3787,8 +3788,8 @@ public final class InputMethodManager { */ public void setExplicitlyEnabledInputMethodSubtypes(@NonNull String imiId, @NonNull int[] subtypeHashCodes) { - mServiceInvoker.setExplicitlyEnabledInputMethodSubtypes(imiId, subtypeHashCodes, - UserHandle.myUserId()); + IInputMethodManagerGlobalInvoker.setExplicitlyEnabledInputMethodSubtypes(imiId, + subtypeHashCodes, UserHandle.myUserId()); } /** @@ -3798,7 +3799,7 @@ public final class InputMethodManager { */ @Nullable public InputMethodSubtype getLastInputMethodSubtype() { - return mServiceInvoker.getLastInputMethodSubtype(UserHandle.myUserId()); + return IInputMethodManagerGlobalInvoker.getLastInputMethodSubtype(UserHandle.myUserId()); } /** diff --git a/core/java/com/android/internal/inputmethod/IInputMethodManagerGlobal.java b/core/java/android/view/inputmethod/InputMethodManagerGlobal.java index 5392bdcec5ee..63d9167bb2f9 100644 --- a/core/java/com/android/internal/inputmethod/IInputMethodManagerGlobal.java +++ b/core/java/android/view/inputmethod/InputMethodManagerGlobal.java @@ -14,64 +14,31 @@ * limitations under the License. */ -package com.android.internal.inputmethod; +package android.view.inputmethod; import android.annotation.AnyThread; -import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresNoPermission; import android.annotation.RequiresPermission; -import android.content.Context; import android.os.RemoteException; -import android.os.ServiceManager; +import com.android.internal.inputmethod.ImeTracing; import com.android.internal.view.IInputMethodManager; import java.util.function.Consumer; /** - * A global wrapper to directly invoke {@link IInputMethodManager} IPCs. + * Defines a set of static methods that can be used globally by framework classes. * - * <p>All public static methods are guaranteed to be thread-safe.</p> - * - * <p>All public methods are guaranteed to do nothing when {@link IInputMethodManager} is - * unavailable.</p> + * @hide */ -public final class IInputMethodManagerGlobal { - @Nullable - private static volatile IInputMethodManager sServiceCache = null; - +public class InputMethodManagerGlobal { /** - * @return {@code true} if {@link IInputMethodManager} is available. + * @return {@code true} if IME tracing is currently is available. */ @AnyThread - public static boolean isAvailable() { - return getService() != null; - } - - @AnyThread - @Nullable - private static IInputMethodManager getService() { - IInputMethodManager service = sServiceCache; - if (service == null) { - service = IInputMethodManager.Stub.asInterface( - ServiceManager.getService(Context.INPUT_METHOD_SERVICE)); - if (service == null) { - return null; - } - sServiceCache = service; - } - return service; - } - - @AnyThread - private static void handleRemoteExceptionOrRethrow(@NonNull RemoteException e, - @Nullable Consumer<RemoteException> exceptionHandler) { - if (exceptionHandler != null) { - exceptionHandler.accept(e); - } else { - throw e.rethrowFromSystemServer(); - } + public static boolean isImeTraceAvailable() { + return IInputMethodManagerGlobalInvoker.isAvailable(); } /** @@ -88,15 +55,7 @@ public final class IInputMethodManagerGlobal { @AnyThread public static void startProtoDump(byte[] protoDump, int source, String where, @Nullable Consumer<RemoteException> exceptionHandler) { - final IInputMethodManager service = getService(); - if (service == null) { - return; - } - try { - service.startProtoDump(protoDump, source, where); - } catch (RemoteException e) { - handleRemoteExceptionOrRethrow(e, exceptionHandler); - } + IInputMethodManagerGlobalInvoker.startProtoDump(protoDump, source, where, exceptionHandler); } /** @@ -107,15 +66,7 @@ public final class IInputMethodManagerGlobal { @RequiresPermission(android.Manifest.permission.CONTROL_UI_TRACING) @AnyThread public static void startImeTrace(@Nullable Consumer<RemoteException> exceptionHandler) { - final IInputMethodManager service = getService(); - if (service == null) { - return; - } - try { - service.startImeTrace(); - } catch (RemoteException e) { - handleRemoteExceptionOrRethrow(e, exceptionHandler); - } + IInputMethodManagerGlobalInvoker.startImeTrace(exceptionHandler); } /** @@ -126,15 +77,7 @@ public final class IInputMethodManagerGlobal { @RequiresPermission(android.Manifest.permission.CONTROL_UI_TRACING) @AnyThread public static void stopImeTrace(@Nullable Consumer<RemoteException> exceptionHandler) { - final IInputMethodManager service = getService(); - if (service == null) { - return; - } - try { - service.stopImeTrace(); - } catch (RemoteException e) { - handleRemoteExceptionOrRethrow(e, exceptionHandler); - } + IInputMethodManagerGlobalInvoker.stopImeTrace(exceptionHandler); } /** @@ -145,31 +88,17 @@ public final class IInputMethodManagerGlobal { @RequiresNoPermission @AnyThread public static boolean isImeTraceEnabled() { - final IInputMethodManager service = getService(); - if (service == null) { - return false; - } - try { - return service.isImeTraceEnabled(); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } + return IInputMethodManagerGlobalInvoker.isImeTraceEnabled(); } /** * Invokes {@link IInputMethodManager#removeImeSurface()} + * + * @param exceptionHandler an optional {@link RemoteException} handler. */ @RequiresPermission(android.Manifest.permission.INTERNAL_SYSTEM_WINDOW) @AnyThread public static void removeImeSurface(@Nullable Consumer<RemoteException> exceptionHandler) { - final IInputMethodManager service = getService(); - if (service == null) { - return; - } - try { - service.removeImeSurface(); - } catch (RemoteException e) { - handleRemoteExceptionOrRethrow(e, exceptionHandler); - } + IInputMethodManagerGlobalInvoker.removeImeSurface(exceptionHandler); } } diff --git a/core/java/android/window/StartingWindowInfo.java b/core/java/android/window/StartingWindowInfo.java index d16103767987..1b64e613ed66 100644 --- a/core/java/android/window/StartingWindowInfo.java +++ b/core/java/android/window/StartingWindowInfo.java @@ -25,7 +25,8 @@ import android.content.pm.ActivityInfo; import android.os.Parcel; import android.os.Parcelable; import android.view.InsetsState; -import android.view.InsetsVisibilities; +import android.view.WindowInsets; +import android.view.WindowInsets.Type.InsetsType; import android.view.WindowManager; /** @@ -181,11 +182,7 @@ public final class StartingWindowInfo implements Parcelable { */ public TaskSnapshot taskSnapshot; - /** - * The requested insets visibility of the top main window. - * @hide - */ - public final InsetsVisibilities requestedVisibilities = new InsetsVisibilities(); + public @InsetsType int requestedVisibleTypes = WindowInsets.Type.defaultVisible(); public StartingWindowInfo() { @@ -218,7 +215,7 @@ public final class StartingWindowInfo implements Parcelable { dest.writeInt(splashScreenThemeResId); dest.writeBoolean(isKeyguardOccluded); dest.writeTypedObject(taskSnapshot, flags); - requestedVisibilities.writeToParcel(dest, flags); + dest.writeInt(requestedVisibleTypes); } void readFromParcel(@NonNull Parcel source) { @@ -232,7 +229,7 @@ public final class StartingWindowInfo implements Parcelable { splashScreenThemeResId = source.readInt(); isKeyguardOccluded = source.readBoolean(); taskSnapshot = source.readTypedObject(TaskSnapshot.CREATOR); - requestedVisibilities.readFromParcel(source); + requestedVisibleTypes = source.readInt(); } @Override diff --git a/core/java/com/android/internal/inputmethod/ImeTracing.java b/core/java/com/android/internal/inputmethod/ImeTracing.java index a4328cc14aa0..e6a9b543dae2 100644 --- a/core/java/com/android/internal/inputmethod/ImeTracing.java +++ b/core/java/com/android/internal/inputmethod/ImeTracing.java @@ -22,6 +22,7 @@ import android.app.ActivityThread; import android.util.Log; import android.util.proto.ProtoOutputStream; import android.view.inputmethod.InputMethodManager; +import android.view.inputmethod.InputMethodManagerGlobal; import java.io.PrintWriter; @@ -44,7 +45,7 @@ public abstract class ImeTracing { private static ImeTracing sInstance; static boolean sEnabled = false; - private final boolean mIsAvailable = IInputMethodManagerGlobal.isAvailable(); + private final boolean mIsAvailable = InputMethodManagerGlobal.isImeTraceAvailable(); protected boolean mDumpInProgress; protected final Object mDumpInProgressLock = new Object(); @@ -81,7 +82,7 @@ public abstract class ImeTracing { * @param where */ public void sendToService(byte[] protoDump, int source, String where) { - IInputMethodManagerGlobal.startProtoDump(protoDump, source, where, + InputMethodManagerGlobal.startProtoDump(protoDump, source, where, e -> Log.e(TAG, "Exception while sending ime-related dump to server", e)); } @@ -90,7 +91,7 @@ public abstract class ImeTracing { */ @RequiresPermission(android.Manifest.permission.CONTROL_UI_TRACING) public final void startImeTrace() { - IInputMethodManagerGlobal.startImeTrace(e -> Log.e(TAG, "Could not start ime trace.", e)); + InputMethodManagerGlobal.startImeTrace(e -> Log.e(TAG, "Could not start ime trace.", e)); } /** @@ -98,7 +99,7 @@ public abstract class ImeTracing { */ @RequiresPermission(android.Manifest.permission.CONTROL_UI_TRACING) public final void stopImeTrace() { - IInputMethodManagerGlobal.stopImeTrace(e -> Log.e(TAG, "Could not stop ime trace.", e)); + InputMethodManagerGlobal.stopImeTrace(e -> Log.e(TAG, "Could not stop ime trace.", e)); } /** diff --git a/core/java/com/android/internal/inputmethod/ImeTracingClientImpl.java b/core/java/com/android/internal/inputmethod/ImeTracingClientImpl.java index 4caca84d76c7..95ed4ed50971 100644 --- a/core/java/com/android/internal/inputmethod/ImeTracingClientImpl.java +++ b/core/java/com/android/internal/inputmethod/ImeTracingClientImpl.java @@ -20,6 +20,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.util.proto.ProtoOutputStream; import android.view.inputmethod.InputMethodManager; +import android.view.inputmethod.InputMethodManagerGlobal; import java.io.PrintWriter; @@ -28,7 +29,7 @@ import java.io.PrintWriter; */ class ImeTracingClientImpl extends ImeTracing { ImeTracingClientImpl() { - sEnabled = IInputMethodManagerGlobal.isImeTraceEnabled(); + sEnabled = InputMethodManagerGlobal.isImeTraceEnabled(); } @Override diff --git a/core/java/com/android/internal/policy/DecorView.java b/core/java/com/android/internal/policy/DecorView.java index a352063aa6ee..3e988e61d978 100644 --- a/core/java/com/android/internal/policy/DecorView.java +++ b/core/java/com/android/internal/policy/DecorView.java @@ -20,8 +20,6 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; import static android.os.Build.VERSION_CODES.M; import static android.os.Build.VERSION_CODES.N; -import static android.view.InsetsState.ITYPE_NAVIGATION_BAR; -import static android.view.InsetsState.ITYPE_STATUS_BAR; import static android.view.InsetsState.clearsCompatInsets; import static android.view.View.MeasureSpec.AT_MOST; import static android.view.View.MeasureSpec.EXACTLY; @@ -79,8 +77,6 @@ import android.view.ActionMode; import android.view.ContextThemeWrapper; import android.view.Gravity; import android.view.InputQueue; -import android.view.InsetsState; -import android.view.InsetsState.InternalInsetsType; import android.view.KeyEvent; import android.view.KeyboardShortcutGroup; import android.view.LayoutInflater; @@ -98,6 +94,7 @@ import android.view.ViewTreeObserver; import android.view.Window; import android.view.WindowCallbacks; import android.view.WindowInsets; +import android.view.WindowInsets.Type.InsetsType; import android.view.WindowInsetsController; import android.view.WindowInsetsController.Appearance; import android.view.WindowManager; @@ -145,13 +142,15 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind new ColorViewAttributes(FLAG_TRANSLUCENT_STATUS, Gravity.TOP, Gravity.LEFT, Gravity.RIGHT, Window.STATUS_BAR_BACKGROUND_TRANSITION_NAME, - com.android.internal.R.id.statusBarBackground, ITYPE_STATUS_BAR); + com.android.internal.R.id.statusBarBackground, + WindowInsets.Type.statusBars()); public static final ColorViewAttributes NAVIGATION_BAR_COLOR_VIEW_ATTRIBUTES = new ColorViewAttributes(FLAG_TRANSLUCENT_NAVIGATION, Gravity.BOTTOM, Gravity.RIGHT, Gravity.LEFT, Window.NAVIGATION_BAR_BACKGROUND_TRANSITION_NAME, - com.android.internal.R.id.navigationBarBackground, ITYPE_NAVIGATION_BAR); + com.android.internal.R.id.navigationBarBackground, + WindowInsets.Type.navigationBars()); // This is used to workaround an issue where the PiP shadow can be transparent if the window // background is transparent @@ -1106,6 +1105,7 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind int sysUiVisibility = attrs.systemUiVisibility | getWindowSystemUiVisibility(); final WindowInsetsController controller = getWindowInsetsController(); + final @InsetsType int requestedVisibleTypes = controller.getRequestedVisibleTypes(); // IME is an exceptional floating window that requires color view. final boolean isImeWindow = @@ -1164,7 +1164,7 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind mWindow.mNavigationBarDividerColor, navBarSize, navBarToRightEdge || navBarToLeftEdge, navBarToLeftEdge, 0 /* sideInset */, animate && !disallowAnimate, - mForceWindowDrawsBarBackgrounds, controller); + mForceWindowDrawsBarBackgrounds, requestedVisibleTypes); boolean oldDrawLegacy = mDrawLegacyNavigationBarBackground; mDrawLegacyNavigationBarBackground = (mWindow.getAttributes().flags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) == 0; @@ -1187,7 +1187,7 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind updateColorViewInt(mStatusColorViewState, statusBarColor, 0, mLastTopInset, false /* matchVertical */, statusBarNeedsLeftInset, statusBarSideInset, animate && !disallowAnimate, - mForceWindowDrawsBarBackgrounds, controller); + mForceWindowDrawsBarBackgrounds, requestedVisibleTypes); if (mHasCaption) { mDecorCaptionView.getCaption().setBackgroundColor(statusBarColor); @@ -1206,7 +1206,7 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind // Note: Once the app uses the R+ Window.setDecorFitsSystemWindows(false) API we no longer // consume insets because they might no longer set SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION. boolean hideNavigation = (sysUiVisibility & SYSTEM_UI_FLAG_HIDE_NAVIGATION) != 0 - || !(controller == null || controller.isRequestedVisible(ITYPE_NAVIGATION_BAR)); + || (requestedVisibleTypes & WindowInsets.Type.navigationBars()) == 0; boolean decorFitsSystemWindows = mWindow.mDecorFitsSystemWindows; boolean forceConsumingNavBar = ((mForceWindowDrawsBarBackgrounds || mDrawLegacyNavigationBarBackgroundHandled) @@ -1226,10 +1226,10 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind // If we didn't request fullscreen layout, but we still got it because of the // mForceWindowDrawsBarBackgrounds flag, also consume top inset. // If we should always consume system bars, only consume that if the app wanted to go to - // fullscreen, as othrewise we can expect the app to handle it. + // fullscreen, as otherwise we can expect the app to handle it. boolean fullscreen = (sysUiVisibility & SYSTEM_UI_FLAG_FULLSCREEN) != 0 || (attrs.flags & FLAG_FULLSCREEN) != 0 - || !(controller == null || controller.isRequestedVisible(ITYPE_STATUS_BAR)); + || (requestedVisibleTypes & WindowInsets.Type.statusBars()) == 0; boolean consumingStatusBar = (sysUiVisibility & SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) == 0 && decorFitsSystemWindows && (attrs.flags & FLAG_LAYOUT_IN_SCREEN) == 0 @@ -1438,10 +1438,10 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind */ private void updateColorViewInt(final ColorViewState state, int color, int dividerColor, int size, boolean verticalBar, boolean seascape, int sideMargin, boolean animate, - boolean force, WindowInsetsController controller) { + boolean force, @InsetsType int requestedVisibleTypes) { state.present = state.attributes.isPresent( - (controller.isRequestedVisible(state.attributes.insetsType) - || mLastShouldAlwaysConsumeSystemBars), + (requestedVisibleTypes & state.attributes.insetsType) != 0 + || mLastShouldAlwaysConsumeSystemBars, mWindow.getAttributes().flags, force); boolean show = state.attributes.isVisible(state.present, color, mWindow.getAttributes().flags, force); @@ -2686,11 +2686,10 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind final int horizontalGravity; final int seascapeGravity; final String transitionName; - final @InternalInsetsType int insetsType; + final @InsetsType int insetsType; private ColorViewAttributes(int translucentFlag, int verticalGravity, int horizontalGravity, - int seascapeGravity, String transitionName, int id, - @InternalInsetsType int insetsType) { + int seascapeGravity, String transitionName, int id, @InsetsType int insetsType) { this.id = id; this.translucentFlag = translucentFlag; this.verticalGravity = verticalGravity; @@ -2707,13 +2706,14 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind public boolean isVisible(boolean present, int color, int windowFlags, boolean force) { return present - && (color & Color.BLACK) != 0 - && ((windowFlags & translucentFlag) == 0 || force); + && Color.alpha(color) != 0 + && ((windowFlags & translucentFlag) == 0 || force); } - public boolean isVisible(InsetsState state, int color, int windowFlags, boolean force) { - final boolean present = isPresent(state.getSource(insetsType).isVisible(), windowFlags, - force); + public boolean isVisible(@InsetsType int requestedVisibleTypes, int color, int windowFlags, + boolean force) { + final boolean requestedVisible = (requestedVisibleTypes & insetsType) != 0; + final boolean present = isPresent(requestedVisible, windowFlags, force); return isVisible(present, color, windowFlags, force); } } diff --git a/core/java/com/android/internal/security/TEST_MAPPING b/core/java/com/android/internal/security/TEST_MAPPING index 9a5e90e8681f..803760c55e71 100644 --- a/core/java/com/android/internal/security/TEST_MAPPING +++ b/core/java/com/android/internal/security/TEST_MAPPING @@ -1,6 +1,17 @@ { "presubmit": [ { + "name": "FrameworksCoreTests", + "options": [ + { + "include-filter": "com.android.internal.security." + }, + { + "include-annotation": "android.platform.test.annotations.Presubmit" + } + ] + }, + { "name": "ApkVerityTest", "file_patterns": ["VerityUtils\\.java"] } diff --git a/core/java/com/android/internal/security/VerityUtils.java b/core/java/com/android/internal/security/VerityUtils.java index cb5820f1ade9..7f45c098daba 100644 --- a/core/java/com/android/internal/security/VerityUtils.java +++ b/core/java/com/android/internal/security/VerityUtils.java @@ -23,10 +23,28 @@ import android.system.Os; import android.system.OsConstants; import android.util.Slog; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.org.bouncycastle.asn1.nist.NISTObjectIdentifiers; +import com.android.internal.org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; +import com.android.internal.org.bouncycastle.cms.CMSException; +import com.android.internal.org.bouncycastle.cms.CMSProcessableByteArray; +import com.android.internal.org.bouncycastle.cms.CMSSignedData; +import com.android.internal.org.bouncycastle.cms.SignerInformation; +import com.android.internal.org.bouncycastle.cms.SignerInformationVerifier; +import com.android.internal.org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder; +import com.android.internal.org.bouncycastle.operator.OperatorCreationException; + import java.io.File; import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; /** Provides fsverity related operations. */ public abstract class VerityUtils { @@ -91,6 +109,91 @@ public abstract class VerityUtils { } /** + * Verifies the signature over the fs-verity digest using the provided certificate. + * + * This method should only be used by any existing fs-verity use cases that require + * PKCS#7 signature verification, if backward compatibility is necessary. + * + * Since PKCS#7 is too flexible, for the current specific need, only specific configuration + * will be accepted: + * <ul> + * <li>Must use SHA256 as the digest algorithm + * <li>Must use rsaEncryption as signature algorithm + * <li>Must be detached / without content + * <li>Must not include any signed or unsigned attributes + * </ul> + * + * It is up to the caller to provide an appropriate/trusted certificate. + * + * @param signatureBlock byte array of a PKCS#7 detached signature + * @param digest fs-verity digest with the common configuration using sha256 + * @param derCertInputStream an input stream of a X.509 certificate in DER + * @return whether the verification succeeds + */ + public static boolean verifyPkcs7DetachedSignature(@NonNull byte[] signatureBlock, + @NonNull byte[] digest, @NonNull InputStream derCertInputStream) { + if (digest.length != 32) { + Slog.w(TAG, "Only sha256 is currently supported"); + return false; + } + + try { + CMSSignedData signedData = new CMSSignedData( + new CMSProcessableByteArray(toFormattedDigest(digest)), + signatureBlock); + + if (!signedData.isDetachedSignature()) { + Slog.w(TAG, "Expect only detached siganture"); + return false; + } + if (!signedData.getCertificates().getMatches(null).isEmpty()) { + Slog.w(TAG, "Expect no certificate in signature"); + return false; + } + if (!signedData.getCRLs().getMatches(null).isEmpty()) { + Slog.w(TAG, "Expect no CRL in signature"); + return false; + } + + X509Certificate trustedCert = (X509Certificate) CertificateFactory.getInstance("X.509") + .generateCertificate(derCertInputStream); + SignerInformationVerifier verifier = new JcaSimpleSignerInfoVerifierBuilder() + .build(trustedCert); + + // Verify any signature with the trusted certificate. + for (SignerInformation si : signedData.getSignerInfos().getSigners()) { + // To be the most strict while dealing with the complicated PKCS#7 signature, reject + // everything we don't need. + if (si.getSignedAttributes() != null && si.getSignedAttributes().size() > 0) { + Slog.w(TAG, "Unexpected signed attributes"); + return false; + } + if (si.getUnsignedAttributes() != null && si.getUnsignedAttributes().size() > 0) { + Slog.w(TAG, "Unexpected unsigned attributes"); + return false; + } + if (!NISTObjectIdentifiers.id_sha256.getId().equals(si.getDigestAlgOID())) { + Slog.w(TAG, "Unsupported digest algorithm OID: " + si.getDigestAlgOID()); + return false; + } + if (!PKCSObjectIdentifiers.rsaEncryption.getId().equals(si.getEncryptionAlgOID())) { + Slog.w(TAG, "Unsupported encryption algorithm OID: " + + si.getEncryptionAlgOID()); + return false; + } + + if (si.verify(verifier)) { + return true; + } + } + return false; + } catch (CertificateException | CMSException | OperatorCreationException e) { + Slog.w(TAG, "Error occurred during the PKCS#7 signature verification", e); + } + return false; + } + + /** * Returns fs-verity digest for the file if enabled, otherwise returns null. The digest is a * hash of root hash of fs-verity's Merkle tree with extra metadata. * @@ -110,6 +213,19 @@ public abstract class VerityUtils { return result; } + /** @hide */ + @VisibleForTesting + public static byte[] toFormattedDigest(byte[] digest) { + // Construct fsverity_formatted_digest used in fs-verity's built-in signature verification. + ByteBuffer buffer = ByteBuffer.allocate(12 + digest.length); // struct size + sha256 size + buffer.order(ByteOrder.LITTLE_ENDIAN); + buffer.put("FSVerity".getBytes(StandardCharsets.US_ASCII)); + buffer.putShort((short) 1); // FS_VERITY_HASH_ALG_SHA256 + buffer.putShort((short) digest.length); + buffer.put(digest); + return buffer.array(); + } + private static native int enableFsverityNative(@NonNull String filePath, @NonNull byte[] pkcs7Signature); private static native int measureFsverityNative(@NonNull String filePath, diff --git a/core/jni/Android.bp b/core/jni/Android.bp index d59a51ab9174..c50abb3ff42a 100644 --- a/core/jni/Android.bp +++ b/core/jni/Android.bp @@ -329,7 +329,6 @@ cc_library_shared { header_libs: [ "bionic_libc_platform_headers", "dnsproxyd_protocol_headers", - "libandroid_runtime_vm_headers", ], }, host: { @@ -418,24 +417,3 @@ cc_library_shared { never: true, }, } - -cc_library_headers { - name: "libandroid_runtime_vm_headers", - host_supported: true, - vendor_available: true, - // TODO(b/153609531): remove when libbinder is not native_bridge_supported - native_bridge_supported: true, - // Allow only modules from the following list to create threads that can be - // attached to the JVM. This list should be a subset of the dependencies of - // libandroid_runtime. - visibility: [ - "//frameworks/native/libs/binder", - ], - export_include_dirs: ["include_vm"], - header_libs: [ - "jni_headers", - ], - export_header_lib_headers: [ - "jni_headers", - ], -} diff --git a/core/jni/AndroidRuntime.cpp b/core/jni/AndroidRuntime.cpp index 422bdc98e9b5..9da28a3e56d2 100644 --- a/core/jni/AndroidRuntime.cpp +++ b/core/jni/AndroidRuntime.cpp @@ -22,7 +22,6 @@ #include <android-base/properties.h> #include <android/graphics/jni_runtime.h> #include <android_runtime/AndroidRuntime.h> -#include <android_runtime/vm.h> #include <assert.h> #include <binder/IBinder.h> #include <binder/IPCThreadState.h> diff --git a/core/jni/android_os_Parcel.cpp b/core/jni/android_os_Parcel.cpp index 1f64df49cb56..4d8dac1daaf0 100644 --- a/core/jni/android_os_Parcel.cpp +++ b/core/jni/android_os_Parcel.cpp @@ -116,6 +116,11 @@ static void android_os_Parcel_markForBinder(JNIEnv* env, jclass clazz, jlong nat } } +static jboolean android_os_Parcel_isForRpc(jlong nativePtr) { + Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr); + return parcel ? parcel->isForRpc() : false; +} + static jint android_os_Parcel_dataSize(jlong nativePtr) { Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr); @@ -808,6 +813,8 @@ static const JNINativeMethod gParcelMethods[] = { // @FastNative {"nativeMarkForBinder", "(JLandroid/os/IBinder;)V", (void*)android_os_Parcel_markForBinder}, // @CriticalNative + {"nativeIsForRpc", "(J)Z", (void*)android_os_Parcel_isForRpc}, + // @CriticalNative {"nativeDataSize", "(J)I", (void*)android_os_Parcel_dataSize}, // @CriticalNative {"nativeDataAvail", "(J)I", (void*)android_os_Parcel_dataAvail}, diff --git a/core/jni/android_view_InputDevice.cpp b/core/jni/android_view_InputDevice.cpp index 39ec0374dc5e..b2994f41af4b 100644 --- a/core/jni/android_view_InputDevice.cpp +++ b/core/jni/android_view_InputDevice.cpp @@ -70,6 +70,8 @@ jobject android_view_InputDevice_create(JNIEnv* env, const InputDeviceInfo& devi deviceInfo.hasMic(), deviceInfo.hasButtonUnderPad(), deviceInfo.hasSensor(), deviceInfo.hasBattery(), deviceInfo.supportsUsi())); + // Note: We do not populate the Bluetooth address into the InputDevice object to avoid leaking + // it to apps that do not have the Bluetooth permission. const std::vector<InputDeviceInfo::MotionRange>& ranges = deviceInfo.getMotionRanges(); for (const InputDeviceInfo::MotionRange& range: ranges) { diff --git a/core/proto/android/server/activitymanagerservice.proto b/core/proto/android/server/activitymanagerservice.proto index 5099dd20a6d5..465000069aab 100644 --- a/core/proto/android/server/activitymanagerservice.proto +++ b/core/proto/android/server/activitymanagerservice.proto @@ -977,6 +977,12 @@ message UserControllerProto { optional int32 profile = 2; } repeated UserProfile user_profile_group_ids = 4; + repeated int32 visible_users_array = 5; + + // current_user contains the id of the current user, while current_profiles contains the ids of + // both the current user and its profiles (if any) + optional int32 current_user = 6; + repeated int32 current_profiles = 7; } // sync with com.android.server.am.AppTimeTracker.java diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 554b15374943..62c584847c0f 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -6622,6 +6622,15 @@ <permission android:name="android.permission.MANAGE_DEVICE_LOCK_STATE" android:protectionLevel="internal|role" /> + <!-- Allows applications to use the long running jobs APIs. + <p>This is a special access permission that can be revoked by the system or the user. + <p>Apps need to target API {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} or above + to be able to request this permission. + <p>Protection level: appop + --> + <permission android:name="android.permission.RUN_LONG_JOBS" + android:protectionLevel="normal|appop"/> + <!-- Attribution for Geofencing service. --> <attribution android:tag="GeofencingService" android:label="@string/geofencing_service"/> <!-- Attribution for Country Detector. --> diff --git a/core/res/res/color/letterbox_background.xml b/core/res/res/color/letterbox_background.xml new file mode 100644 index 000000000000..955948ad2b6a --- /dev/null +++ b/core/res/res/color/letterbox_background.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2022 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:color="@color/system_neutral1_500" android:lStar="5" /> +</selector> diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 173908d97b56..23dd1b407b00 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -5217,7 +5217,7 @@ but isn't supported on the device or both dark scrim alpha and blur radius aren't provided. --> - <color name="config_letterboxBackgroundColor">@android:color/system_neutral2_900</color> + <color name="config_letterboxBackgroundColor">@color/letterbox_background</color> <!-- Horizontal position of a center of the letterboxed app window. 0 corresponds to the left side of the screen and 1 to the right side. If given value < 0 diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/TunerAdapterTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/TunerAdapterTest.java new file mode 100644 index 000000000000..fe3ab625bacc --- /dev/null +++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/TunerAdapterTest.java @@ -0,0 +1,433 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.hardware.radio.tests.unittests; + +import static com.google.common.truth.Truth.assertWithMessage; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.graphics.Bitmap; +import android.hardware.radio.IRadioService; +import android.hardware.radio.ITuner; +import android.hardware.radio.ITunerCallback; +import android.hardware.radio.ProgramSelector; +import android.hardware.radio.RadioManager; +import android.hardware.radio.RadioMetadata; +import android.hardware.radio.RadioTuner; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +@RunWith(MockitoJUnitRunner.class) +public final class TunerAdapterTest { + + private static final int CALLBACK_TIMEOUT_MS = 30_000; + private static final int AM_LOWER_LIMIT_KHZ = 150; + + private static final RadioManager.BandConfig TEST_BAND_CONFIG = createBandConfig(); + + private static final ProgramSelector.Identifier FM_IDENTIFIER = + new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, + /* value= */ 94300); + private static final ProgramSelector FM_SELECTOR = + new ProgramSelector(ProgramSelector.PROGRAM_TYPE_FM, FM_IDENTIFIER, + /* secondaryIds= */ null, /* vendorIds= */ null); + private static final RadioManager.ProgramInfo FM_PROGRAM_INFO = createFmProgramInfo(); + + private RadioTuner mRadioTuner; + private ITunerCallback mTunerCallback; + + @Mock + private IRadioService mRadioServiceMock; + @Mock + private Context mContextMock; + @Mock + private ITuner mTunerMock; + @Mock + private RadioTuner.Callback mCallbackMock; + + @Before + public void setUp() throws Exception { + RadioManager radioManager = new RadioManager(mContextMock, mRadioServiceMock); + + doAnswer(invocation -> { + mTunerCallback = (ITunerCallback) invocation.getArguments()[3]; + return mTunerMock; + }).when(mRadioServiceMock).openTuner(anyInt(), any(), anyBoolean(), any()); + + doAnswer(invocation -> { + ProgramSelector program = (ProgramSelector) invocation.getArguments()[0]; + if (program.getPrimaryId().getType() + != ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY) { + throw new IllegalArgumentException(); + } + if (program.getPrimaryId().getValue() < AM_LOWER_LIMIT_KHZ) { + mTunerCallback.onTuneFailed(RadioManager.STATUS_BAD_VALUE, program); + } else { + mTunerCallback.onCurrentProgramInfoChanged(FM_PROGRAM_INFO); + } + return RadioManager.STATUS_OK; + }).when(mTunerMock).tune(any()); + + mRadioTuner = radioManager.openTuner(/* moduleId= */ 0, TEST_BAND_CONFIG, + /* withAudio= */ true, mCallbackMock, /* handler= */ null); + } + + @After + public void cleanUp() throws Exception { + mRadioTuner.close(); + } + + @Test + public void close_forTunerAdapter() throws Exception { + mRadioTuner.close(); + + verify(mTunerMock).close(); + } + + @Test + public void setConfiguration_forTunerAdapter() throws Exception { + int status = mRadioTuner.setConfiguration(TEST_BAND_CONFIG); + + verify(mTunerMock).setConfiguration(TEST_BAND_CONFIG); + assertWithMessage("Status for setting configuration") + .that(status).isEqualTo(RadioManager.STATUS_OK); + } + + @Test + public void getConfiguration_forTunerAdapter() throws Exception { + when(mTunerMock.getConfiguration()).thenReturn(TEST_BAND_CONFIG); + RadioManager.BandConfig[] bandConfigs = new RadioManager.BandConfig[1]; + + int status = mRadioTuner.getConfiguration(bandConfigs); + + assertWithMessage("Status for getting configuration") + .that(status).isEqualTo(RadioManager.STATUS_OK); + assertWithMessage("Configuration obtained from radio tuner") + .that(bandConfigs[0]).isEqualTo(TEST_BAND_CONFIG); + } + + @Test + public void setMute_forTunerAdapter() { + int status = mRadioTuner.setMute(/* mute= */ true); + + assertWithMessage("Status for setting mute") + .that(status).isEqualTo(RadioManager.STATUS_OK); + } + + @Test + public void getMute_forTunerAdapter() throws Exception { + when(mTunerMock.isMuted()).thenReturn(true); + + boolean muteStatus = mRadioTuner.getMute(); + + assertWithMessage("Mute status").that(muteStatus).isTrue(); + } + + @Test + public void step_forTunerAdapter_succeeds() throws Exception { + doAnswer(invocation -> { + mTunerCallback.onCurrentProgramInfoChanged(FM_PROGRAM_INFO); + return RadioManager.STATUS_OK; + }).when(mTunerMock).step(anyBoolean(), anyBoolean()); + + int scanStatus = mRadioTuner.step(RadioTuner.DIRECTION_DOWN, /* skipSubChannel= */ false); + + verify(mTunerMock).step(/* skipSubChannel= */ true, /* skipSubChannel= */ false); + assertWithMessage("Status for stepping") + .that(scanStatus).isEqualTo(RadioManager.STATUS_OK); + verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS)).onProgramInfoChanged(FM_PROGRAM_INFO); + } + + @Test + public void seek_forTunerAdapter_succeeds() throws Exception { + doAnswer(invocation -> { + mTunerCallback.onCurrentProgramInfoChanged(FM_PROGRAM_INFO); + return RadioManager.STATUS_OK; + }).when(mTunerMock).scan(anyBoolean(), anyBoolean()); + + int scanStatus = mRadioTuner.scan(RadioTuner.DIRECTION_DOWN, /* skipSubChannel= */ false); + + verify(mTunerMock).scan(/* directionDown= */ true, /* skipSubChannel= */ false); + assertWithMessage("Status for seeking") + .that(scanStatus).isEqualTo(RadioManager.STATUS_OK); + verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS)).onProgramInfoChanged(FM_PROGRAM_INFO); + } + + @Test + public void seek_forTunerAdapter_invokesOnErrorWhenTimeout() throws Exception { + doAnswer(invocation -> { + mTunerCallback.onError(RadioTuner.ERROR_SCAN_TIMEOUT); + return RadioManager.STATUS_OK; + }).when(mTunerMock).scan(anyBoolean(), anyBoolean()); + + mRadioTuner.scan(RadioTuner.DIRECTION_UP, /* skipSubChannel*/ true); + + verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS)).onError(RadioTuner.ERROR_SCAN_TIMEOUT); + } + + @Test + public void tune_withChannelsForTunerAdapter_succeeds() { + int status = mRadioTuner.tune(/* channel= */ 92300, /* subChannel= */ 0); + + assertWithMessage("Status for tuning with channel and sub-channel") + .that(status).isEqualTo(RadioManager.STATUS_OK); + verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS)).onProgramInfoChanged(FM_PROGRAM_INFO); + } + + @Test + public void tune_withValidSelectorForTunerAdapter_succeeds() throws Exception { + mRadioTuner.tune(FM_SELECTOR); + + verify(mTunerMock).tune(FM_SELECTOR); + verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS)).onProgramInfoChanged(FM_PROGRAM_INFO); + } + + + @Test + public void tune_withInvalidSelectorForTunerAdapter_invokesOnTuneFailed() { + ProgramSelector invalidSelector = new ProgramSelector(ProgramSelector.PROGRAM_TYPE_FM, + new ProgramSelector.Identifier( + ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, /* value= */ 100), + /* secondaryIds= */ null, /* vendorIds= */ null); + + mRadioTuner.tune(invalidSelector); + + verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS)) + .onTuneFailed(RadioManager.STATUS_BAD_VALUE, invalidSelector); + } + + @Test + public void cancel_forTunerAdapter() throws Exception { + mRadioTuner.tune(FM_SELECTOR); + + mRadioTuner.cancel(); + + verify(mTunerMock).cancel(); + } + + @Test + public void cancelAnnouncement_forTunerAdapter() throws Exception { + mRadioTuner.cancelAnnouncement(); + + verify(mTunerMock).cancelAnnouncement(); + } + + @Test + public void getProgramInfo_beforeProgramInfoSetForTunerAdapter() { + RadioManager.ProgramInfo[] programInfoArray = new RadioManager.ProgramInfo[1]; + + int status = mRadioTuner.getProgramInformation(programInfoArray); + + assertWithMessage("Status for getting null program info") + .that(status).isEqualTo(RadioManager.STATUS_INVALID_OPERATION); + } + + @Test + public void getProgramInfo_afterTuneForTunerAdapter() { + mRadioTuner.tune(FM_SELECTOR); + verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS)).onProgramInfoChanged(FM_PROGRAM_INFO); + RadioManager.ProgramInfo[] programInfoArray = new RadioManager.ProgramInfo[1]; + + int status = mRadioTuner.getProgramInformation(programInfoArray); + + assertWithMessage("Status for getting program info") + .that(status).isEqualTo(RadioManager.STATUS_OK); + assertWithMessage("Program info obtained from radio tuner") + .that(programInfoArray[0]).isEqualTo(FM_PROGRAM_INFO); + } + + @Test + public void getMetadataImage_forTunerAdapter() throws Exception { + Bitmap bitmapExpected = Mockito.mock(Bitmap.class); + when(mTunerMock.getImage(anyInt())).thenReturn(bitmapExpected); + int imageId = 1; + + Bitmap image = mRadioTuner.getMetadataImage(/* id= */ imageId); + + assertWithMessage("Image obtained from id %s", imageId) + .that(image).isEqualTo(bitmapExpected); + } + + @Test + public void isAnalogForced_forTunerAdapter() throws Exception { + when(mTunerMock.isConfigFlagSet(RadioManager.CONFIG_FORCE_ANALOG)).thenReturn(true); + + boolean isAnalogForced = mRadioTuner.isAnalogForced(); + + assertWithMessage("Forced analog playback switch") + .that(isAnalogForced).isTrue(); + } + + @Test + public void setAnalogForced_forTunerAdapter() throws Exception { + boolean analogForced = true; + + mRadioTuner.setAnalogForced(analogForced); + + verify(mTunerMock).setConfigFlag(RadioManager.CONFIG_FORCE_ANALOG, analogForced); + } + + @Test + public void isConfigFlagSupported_forTunerAdapter() throws Exception { + when(mTunerMock.isConfigFlagSupported(RadioManager.CONFIG_DAB_DAB_LINKING)) + .thenReturn(true); + + boolean dabFmSoftLinking = + mRadioTuner.isConfigFlagSupported(RadioManager.CONFIG_DAB_DAB_LINKING); + + assertWithMessage("Support for DAB-DAB linking config flag") + .that(dabFmSoftLinking).isTrue(); + } + + @Test + public void isConfigFlagSet_forTunerAdapter() throws Exception { + when(mTunerMock.isConfigFlagSet(RadioManager.CONFIG_DAB_FM_SOFT_LINKING)) + .thenReturn(true); + + boolean dabFmSoftLinking = + mRadioTuner.isConfigFlagSet(RadioManager.CONFIG_DAB_FM_SOFT_LINKING); + + assertWithMessage("DAB-FM soft linking config flag") + .that(dabFmSoftLinking).isTrue(); + } + + @Test + public void setConfigFlag_forTunerAdapter() throws Exception { + boolean dabFmLinking = true; + + mRadioTuner.setConfigFlag(RadioManager.CONFIG_DAB_FM_LINKING, dabFmLinking); + + verify(mTunerMock).setConfigFlag(RadioManager.CONFIG_DAB_FM_LINKING, dabFmLinking); + } + + @Test + public void getParameters_forTunerAdapter() throws Exception { + List<String> parameterKeys = Arrays.asList("ParameterKeyMock"); + Map<String, String> parameters = Map.of("ParameterKeyMock", "ParameterValueMock"); + when(mTunerMock.getParameters(parameterKeys)).thenReturn(parameters); + + assertWithMessage("Parameters obtained from radio tuner") + .that(mRadioTuner.getParameters(parameterKeys)).isEqualTo(parameters); + } + + @Test + public void setParameters_forTunerAdapter() throws Exception { + Map<String, String> parameters = Map.of("ParameterKeyMock", "ParameterValueMock"); + when(mTunerMock.setParameters(parameters)).thenReturn(parameters); + + assertWithMessage("Parameters set for radio tuner") + .that(mRadioTuner.setParameters(parameters)).isEqualTo(parameters); + } + + @Test + public void isAntennaConnected_forTunerAdapter() throws Exception { + mTunerCallback.onAntennaState(/* connected= */ false); + + assertWithMessage("Antenna connection status") + .that(mRadioTuner.isAntennaConnected()).isFalse(); + } + + @Test + public void hasControl_forTunerAdapter() throws Exception { + when(mTunerMock.isClosed()).thenReturn(true); + + assertWithMessage("Control on tuner").that(mRadioTuner.hasControl()).isFalse(); + } + + @Test + public void onConfigurationChanged_forTunerCallbackAdapter() throws Exception { + mTunerCallback.onConfigurationChanged(TEST_BAND_CONFIG); + + verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS)) + .onConfigurationChanged(TEST_BAND_CONFIG); + } + + @Test + public void onTrafficAnnouncement_forTunerCallbackAdapter() throws Exception { + mTunerCallback.onTrafficAnnouncement(/* active= */ true); + + verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS)) + .onTrafficAnnouncement(/* active= */ true); + } + + @Test + public void onEmergencyAnnouncement_forTunerCallbackAdapter() throws Exception { + mTunerCallback.onEmergencyAnnouncement(/* active= */ true); + + verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS)) + .onEmergencyAnnouncement(/* active= */ true); + } + + @Test + public void onBackgroundScanAvailabilityChange_forTunerCallbackAdapter() throws Exception { + mTunerCallback.onBackgroundScanAvailabilityChange(/* isAvailable= */ false); + + verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS)) + .onBackgroundScanAvailabilityChange(/* isAvailable= */ false); + } + + @Test + public void onProgramListChanged_forTunerCallbackAdapter() throws Exception { + mTunerCallback.onProgramListChanged(); + + verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS)).onProgramListChanged(); + } + + @Test + public void onParametersUpdated_forTunerCallbackAdapter() throws Exception { + Map<String, String> parametersExpected = Map.of("ParameterKeyMock", "ParameterValueMock"); + + mTunerCallback.onParametersUpdated(parametersExpected); + + verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS)).onParametersUpdated(parametersExpected); + } + + private static RadioManager.ProgramInfo createFmProgramInfo() { + return new RadioManager.ProgramInfo(FM_SELECTOR, FM_IDENTIFIER, FM_IDENTIFIER, + /* relatedContent= */ null, /* infoFlags= */ 0b110001, + /* signalQuality= */ 1, createRadioMetadata(), /* vendorInfo= */ null); + } + + private static RadioManager.FmBandConfig createBandConfig() { + return new RadioManager.FmBandConfig(new RadioManager.FmBandDescriptor( + RadioManager.REGION_ITU_1, RadioManager.BAND_FM, /* lowerLimit= */ 87500, + /* upperLimit= */ 108000, /* spacing= */ 200, /* stereo= */ true, + /* rds= */ false, /* ta= */ false, /* af= */ false, /* es= */ false)); + } + + private static RadioMetadata createRadioMetadata() { + RadioMetadata.Builder metadataBuilder = new RadioMetadata.Builder(); + return metadataBuilder.putString(RadioMetadata.METADATA_KEY_ARTIST, "artistMock").build(); + } +} diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ConversionUtilsTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ConversionUtilsTest.java new file mode 100644 index 000000000000..31195548c968 --- /dev/null +++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ConversionUtilsTest.java @@ -0,0 +1,221 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.broadcastradio.aidl; + +import android.hardware.broadcastradio.AmFmBandRange; +import android.hardware.broadcastradio.AmFmRegionConfig; +import android.hardware.broadcastradio.DabTableEntry; +import android.hardware.broadcastradio.IdentifierType; +import android.hardware.broadcastradio.Properties; +import android.hardware.broadcastradio.VendorKeyValue; +import android.hardware.radio.Announcement; +import android.hardware.radio.ProgramSelector; +import android.hardware.radio.RadioManager; + +import com.google.common.truth.Expect; + +import org.junit.Rule; +import org.junit.Test; + +import java.util.Map; + +public final class ConversionUtilsTest { + + private static final int FM_LOWER_LIMIT = 87500; + private static final int FM_UPPER_LIMIT = 108000; + private static final int FM_SPACING = 200; + private static final int AM_LOWER_LIMIT = 540; + private static final int AM_UPPER_LIMIT = 1700; + private static final int AM_SPACING = 10; + private static final String DAB_ENTRY_LABEL_1 = "5A"; + private static final int DAB_ENTRY_FREQUENCY_1 = 174928; + private static final String DAB_ENTRY_LABEL_2 = "12D"; + private static final int DAB_ENTRY_FREQUENCY_2 = 229072; + private static final String VENDOR_INFO_KEY_1 = "vendorKey1"; + private static final String VENDOR_INFO_VALUE_1 = "vendorValue1"; + private static final String VENDOR_INFO_KEY_2 = "vendorKey2"; + private static final String VENDOR_INFO_VALUE_2 = "vendorValue2"; + private static final String TEST_SERVICE_NAME = "serviceMock"; + private static final int TEST_ID = 1; + private static final String TEST_MAKER = "makerMock"; + private static final String TEST_PRODUCT = "productMock"; + private static final String TEST_VERSION = "versionMock"; + private static final String TEST_SERIAL = "serialMock"; + + private static final int TEST_ENABLED_TYPE = Announcement.TYPE_EMERGENCY; + private static final int TEST_ANNOUNCEMENT_FREQUENCY = FM_LOWER_LIMIT + FM_SPACING; + + private static final RadioManager.ModuleProperties MODULE_PROPERTIES = + convertToModuleProperties(); + private static final Announcement ANNOUNCEMENT = + ConversionUtils.announcementFromHalAnnouncement( + AidlTestUtils.makeAnnouncement(TEST_ENABLED_TYPE, TEST_ANNOUNCEMENT_FREQUENCY)); + + @Rule + public final Expect expect = Expect.create(); + + @Test + public void propertiesFromHalProperties_idsMatch() { + expect.withMessage("Properties id") + .that(MODULE_PROPERTIES.getId()).isEqualTo(TEST_ID); + } + + @Test + public void propertiesFromHalProperties_serviceNamesMatch() { + expect.withMessage("Service name") + .that(MODULE_PROPERTIES.getServiceName()).isEqualTo(TEST_SERVICE_NAME); + } + + @Test + public void propertiesFromHalProperties_implementorsMatch() { + expect.withMessage("Implementor") + .that(MODULE_PROPERTIES.getImplementor()).isEqualTo(TEST_MAKER); + } + + + @Test + public void propertiesFromHalProperties_productsMatch() { + expect.withMessage("Product") + .that(MODULE_PROPERTIES.getProduct()).isEqualTo(TEST_PRODUCT); + } + + @Test + public void propertiesFromHalProperties_versionsMatch() { + expect.withMessage("Version") + .that(MODULE_PROPERTIES.getVersion()).isEqualTo(TEST_VERSION); + } + + @Test + public void propertiesFromHalProperties_serialsMatch() { + expect.withMessage("Serial") + .that(MODULE_PROPERTIES.getSerial()).isEqualTo(TEST_SERIAL); + } + + @Test + public void propertiesFromHalProperties_dabTableInfoMatch() { + Map<String, Integer> dabTableExpected = Map.of(DAB_ENTRY_LABEL_1, DAB_ENTRY_FREQUENCY_1, + DAB_ENTRY_LABEL_2, DAB_ENTRY_FREQUENCY_2); + + expect.withMessage("Supported program types") + .that(MODULE_PROPERTIES.getDabFrequencyTable()) + .containsExactlyEntriesIn(dabTableExpected); + } + + @Test + public void propertiesFromHalProperties_vendorInfoMatch() { + Map<String, String> vendorInfoExpected = Map.of(VENDOR_INFO_KEY_1, VENDOR_INFO_VALUE_1, + VENDOR_INFO_KEY_2, VENDOR_INFO_VALUE_2); + + expect.withMessage("Vendor info").that(MODULE_PROPERTIES.getVendorInfo()) + .containsExactlyEntriesIn(vendorInfoExpected); + } + + @Test + public void propertiesFromHalProperties_bandsMatch() { + RadioManager.BandDescriptor[] bands = MODULE_PROPERTIES.getBands(); + + expect.withMessage("Band descriptors").that(bands).hasLength(2); + + expect.withMessage("FM band frequency lower limit") + .that(bands[0].getLowerLimit()).isEqualTo(FM_LOWER_LIMIT); + expect.withMessage("FM band frequency upper limit") + .that(bands[0].getUpperLimit()).isEqualTo(FM_UPPER_LIMIT); + expect.withMessage("FM band frequency spacing") + .that(bands[0].getSpacing()).isEqualTo(FM_SPACING); + + expect.withMessage("AM band frequency lower limit") + .that(bands[1].getLowerLimit()).isEqualTo(AM_LOWER_LIMIT); + expect.withMessage("AM band frequency upper limit") + .that(bands[1].getUpperLimit()).isEqualTo(AM_UPPER_LIMIT); + expect.withMessage("AM band frequency spacing") + .that(bands[1].getSpacing()).isEqualTo(AM_SPACING); + } + + @Test + public void announcementFromHalAnnouncement_typesMatch() { + expect.withMessage("Announcement type") + .that(ANNOUNCEMENT.getType()).isEqualTo(TEST_ENABLED_TYPE); + } + + @Test + public void announcementFromHalAnnouncement_selectorsMatch() { + ProgramSelector.Identifier primaryIdExpected = new ProgramSelector.Identifier( + ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, TEST_ANNOUNCEMENT_FREQUENCY); + + ProgramSelector selector = ANNOUNCEMENT.getSelector(); + + expect.withMessage("Primary id of announcement selector") + .that(selector.getPrimaryId()).isEqualTo(primaryIdExpected); + expect.withMessage("Secondary ids of announcement selector") + .that(selector.getSecondaryIds()).isEmpty(); + } + + @Test + public void announcementFromHalAnnouncement_VendorInfoMatch() { + expect.withMessage("Announcement vendor info") + .that(ANNOUNCEMENT.getVendorInfo()).isEmpty(); + } + + private static RadioManager.ModuleProperties convertToModuleProperties() { + AmFmRegionConfig amFmConfig = createAmFmRegionConfig(); + DabTableEntry[] dabTableEntries = new DabTableEntry[]{ + createDabTableEntry(DAB_ENTRY_LABEL_1, DAB_ENTRY_FREQUENCY_1), + createDabTableEntry(DAB_ENTRY_LABEL_2, DAB_ENTRY_FREQUENCY_2)}; + Properties properties = createHalProperties(); + + return ConversionUtils.propertiesFromHalProperties(TEST_ID, TEST_SERVICE_NAME, properties, + amFmConfig, dabTableEntries); + } + + private static AmFmRegionConfig createAmFmRegionConfig() { + AmFmRegionConfig amFmRegionConfig = new AmFmRegionConfig(); + amFmRegionConfig.ranges = new AmFmBandRange[]{ + createAmFmBandRange(FM_LOWER_LIMIT, FM_UPPER_LIMIT, FM_SPACING), + createAmFmBandRange(AM_LOWER_LIMIT, AM_UPPER_LIMIT, AM_SPACING)}; + return amFmRegionConfig; + } + + private static AmFmBandRange createAmFmBandRange(int lowerBound, int upperBound, int spacing) { + AmFmBandRange bandRange = new AmFmBandRange(); + bandRange.lowerBound = lowerBound; + bandRange.upperBound = upperBound; + bandRange.spacing = spacing; + bandRange.seekSpacing = bandRange.spacing; + return bandRange; + } + + private static DabTableEntry createDabTableEntry(String label, int value) { + DabTableEntry dabTableEntry = new DabTableEntry(); + dabTableEntry.label = label; + dabTableEntry.frequencyKhz = value; + return dabTableEntry; + } + + private static Properties createHalProperties() { + Properties halProperties = new Properties(); + halProperties.supportedIdentifierTypes = new int[]{IdentifierType.AMFM_FREQUENCY_KHZ, + IdentifierType.RDS_PI, IdentifierType.DAB_SID_EXT}; + halProperties.maker = TEST_MAKER; + halProperties.product = TEST_PRODUCT; + halProperties.version = TEST_VERSION; + halProperties.serial = TEST_SERIAL; + halProperties.vendorInfo = new VendorKeyValue[]{ + AidlTestUtils.makeVendorKeyValue(VENDOR_INFO_KEY_1, VENDOR_INFO_VALUE_1), + AidlTestUtils.makeVendorKeyValue(VENDOR_INFO_KEY_2, VENDOR_INFO_VALUE_2)}; + return halProperties; + } +} diff --git a/core/tests/coretests/Android.bp b/core/tests/coretests/Android.bp index a767f834c765..48cfc87d7d55 100644 --- a/core/tests/coretests/Android.bp +++ b/core/tests/coretests/Android.bp @@ -82,6 +82,7 @@ android_test { resource_dirs: ["res"], resource_zips: [":FrameworksCoreTests_apks_as_resources"], + java_resources: [":ApkVerityTestCertDer"], data: [ ":BstatsTestApp", diff --git a/core/tests/coretests/OWNERS b/core/tests/coretests/OWNERS index 0fb0c3021486..e8c9fe70272d 100644 --- a/core/tests/coretests/OWNERS +++ b/core/tests/coretests/OWNERS @@ -1 +1,4 @@ include platform/frameworks/base:/services/core/java/com/android/server/am/OWNERS + +per-file BinderTest.java = file:platform/frameworks/native:/libs/binder/OWNERS +per-file ParcelTest.java = file:platform/frameworks/native:/libs/binder/OWNERS diff --git a/core/tests/coretests/res/raw/fsverity_sig b/core/tests/coretests/res/raw/fsverity_sig Binary files differnew file mode 100644 index 000000000000..b2f335dc0342 --- /dev/null +++ b/core/tests/coretests/res/raw/fsverity_sig diff --git a/core/tests/coretests/src/android/app/backup/BackupRestoreEventLoggerTest.java b/core/tests/coretests/src/android/app/backup/BackupRestoreEventLoggerTest.java new file mode 100644 index 000000000000..67b24ec17a27 --- /dev/null +++ b/core/tests/coretests/src/android/app/backup/BackupRestoreEventLoggerTest.java @@ -0,0 +1,277 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.backup; + +import static android.app.backup.BackupRestoreEventLogger.OperationType.BACKUP; +import static android.app.backup.BackupRestoreEventLogger.OperationType.RESTORE; + +import static com.google.common.truth.Truth.assertThat; + +import static junit.framework.Assert.fail; + +import android.app.backup.BackupRestoreEventLogger.BackupRestoreDataType; +import android.app.backup.BackupRestoreEventLogger.DataTypeResult; +import android.platform.test.annotations.Presubmit; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +@Presubmit +@RunWith(AndroidJUnit4.class) +public class BackupRestoreEventLoggerTest { + private static final int DATA_TYPES_ALLOWED = 15; + + private static final String DATA_TYPE_1 = "data_type_1"; + private static final String DATA_TYPE_2 = "data_type_2"; + private static final String ERROR_1 = "error_1"; + private static final String ERROR_2 = "error_2"; + private static final String METADATA_1 = "metadata_1"; + private static final String METADATA_2 = "metadata_2"; + + private BackupRestoreEventLogger mLogger; + private MessageDigest mHashDigest; + + @Before + public void setUp() throws Exception { + mHashDigest = MessageDigest.getInstance("SHA-256"); + } + + @Test + public void testBackupLogger_rejectsRestoreLogs() { + mLogger = new BackupRestoreEventLogger(BACKUP); + + assertThat(mLogger.logItemsRestored(DATA_TYPE_1, /* count */ 5)).isFalse(); + assertThat(mLogger.logItemsRestoreFailed(DATA_TYPE_1, /* count */ 5, ERROR_1)).isFalse(); + assertThat(mLogger.logRestoreMetadata(DATA_TYPE_1, /* metadata */ "metadata")).isFalse(); + } + + @Test + public void testRestoreLogger_rejectsBackupLogs() { + mLogger = new BackupRestoreEventLogger(RESTORE); + + assertThat(mLogger.logItemsBackedUp(DATA_TYPE_1, /* count */ 5)).isFalse(); + assertThat(mLogger.logItemsBackupFailed(DATA_TYPE_1, /* count */ 5, ERROR_1)).isFalse(); + assertThat(mLogger.logBackupMetaData(DATA_TYPE_1, /* metadata */ "metadata")).isFalse(); + } + + @Test + public void testBackupLogger_onlyAcceptsAllowedNumberOfDataTypes() { + mLogger = new BackupRestoreEventLogger(BACKUP); + + for (int i = 0; i < DATA_TYPES_ALLOWED; i++) { + String dataType = DATA_TYPE_1 + i; + assertThat(mLogger.logItemsBackedUp(dataType, /* count */ 5)).isTrue(); + assertThat(mLogger.logItemsBackupFailed(dataType, /* count */ 5, /* error */ null)) + .isTrue(); + assertThat(mLogger.logBackupMetaData(dataType, METADATA_1)).isTrue(); + } + + assertThat(mLogger.logItemsBackedUp(DATA_TYPE_2, /* count */ 5)).isFalse(); + assertThat(mLogger.logItemsBackupFailed(DATA_TYPE_2, /* count */ 5, /* error */ null)) + .isFalse(); + assertThat(mLogger.logRestoreMetadata(DATA_TYPE_2, METADATA_1)).isFalse(); + assertThat(getResultForDataTypeIfPresent(mLogger, DATA_TYPE_2)).isEqualTo(Optional.empty()); + } + + @Test + public void testRestoreLogger_onlyAcceptsAllowedNumberOfDataTypes() { + mLogger = new BackupRestoreEventLogger(RESTORE); + + for (int i = 0; i < DATA_TYPES_ALLOWED; i++) { + String dataType = DATA_TYPE_1 + i; + assertThat(mLogger.logItemsRestored(dataType, /* count */ 5)).isTrue(); + assertThat(mLogger.logItemsRestoreFailed(dataType, /* count */ 5, /* error */ null)) + .isTrue(); + assertThat(mLogger.logRestoreMetadata(dataType, METADATA_1)).isTrue(); + } + + assertThat(mLogger.logItemsRestored(DATA_TYPE_2, /* count */ 5)).isFalse(); + assertThat(mLogger.logItemsRestoreFailed(DATA_TYPE_2, /* count */ 5, /* error */ null)) + .isFalse(); + assertThat(mLogger.logRestoreMetadata(DATA_TYPE_2, METADATA_1)).isFalse(); + assertThat(getResultForDataTypeIfPresent(mLogger, DATA_TYPE_2)).isEqualTo(Optional.empty()); + } + + @Test + public void testLogBackupMetadata_repeatedCalls_recordsLatestMetadataHash() { + mLogger = new BackupRestoreEventLogger(BACKUP); + + mLogger.logBackupMetaData(DATA_TYPE_1, METADATA_1); + mLogger.logBackupMetaData(DATA_TYPE_1, METADATA_2); + + byte[] recordedHash = getResultForDataType(mLogger, DATA_TYPE_1).getMetadataHash(); + byte[] expectedHash = getMetaDataHash(METADATA_2); + assertThat(Arrays.equals(recordedHash, expectedHash)).isTrue(); + } + + @Test + public void testLogRestoreMetadata_repeatedCalls_recordsLatestMetadataHash() { + mLogger = new BackupRestoreEventLogger(RESTORE); + + mLogger.logRestoreMetadata(DATA_TYPE_1, METADATA_1); + mLogger.logRestoreMetadata(DATA_TYPE_1, METADATA_2); + + byte[] recordedHash = getResultForDataType(mLogger, DATA_TYPE_1).getMetadataHash(); + byte[] expectedHash = getMetaDataHash(METADATA_2); + assertThat(Arrays.equals(recordedHash, expectedHash)).isTrue(); + } + + @Test + public void testLogItemsBackedUp_repeatedCalls_recordsTotalItems() { + mLogger = new BackupRestoreEventLogger(BACKUP); + + int firstCount = 10; + int secondCount = 5; + mLogger.logItemsBackedUp(DATA_TYPE_1, firstCount); + mLogger.logItemsBackedUp(DATA_TYPE_1, secondCount); + + int dataTypeCount = getResultForDataType(mLogger, DATA_TYPE_1).getSuccessCount(); + assertThat(dataTypeCount).isEqualTo(firstCount + secondCount); + } + + @Test + public void testLogItemsRestored_repeatedCalls_recordsTotalItems() { + mLogger = new BackupRestoreEventLogger(RESTORE); + + int firstCount = 10; + int secondCount = 5; + mLogger.logItemsRestored(DATA_TYPE_1, firstCount); + mLogger.logItemsRestored(DATA_TYPE_1, secondCount); + + int dataTypeCount = getResultForDataType(mLogger, DATA_TYPE_1).getSuccessCount(); + assertThat(dataTypeCount).isEqualTo(firstCount + secondCount); + } + + @Test + public void testLogItemsBackedUp_multipleDataTypes_recordsEachDataType() { + mLogger = new BackupRestoreEventLogger(BACKUP); + + int firstCount = 10; + int secondCount = 5; + mLogger.logItemsBackedUp(DATA_TYPE_1, firstCount); + mLogger.logItemsBackedUp(DATA_TYPE_2, secondCount); + + int firstDataTypeCount = getResultForDataType(mLogger, DATA_TYPE_1).getSuccessCount(); + int secondDataTypeCount = getResultForDataType(mLogger, DATA_TYPE_2).getSuccessCount(); + assertThat(firstDataTypeCount).isEqualTo(firstCount); + assertThat(secondDataTypeCount).isEqualTo(secondCount); + } + + @Test + public void testLogItemsRestored_multipleDataTypes_recordsEachDataType() { + mLogger = new BackupRestoreEventLogger(RESTORE); + + int firstCount = 10; + int secondCount = 5; + mLogger.logItemsRestored(DATA_TYPE_1, firstCount); + mLogger.logItemsRestored(DATA_TYPE_2, secondCount); + + int firstDataTypeCount = getResultForDataType(mLogger, DATA_TYPE_1).getSuccessCount(); + int secondDataTypeCount = getResultForDataType(mLogger, DATA_TYPE_2).getSuccessCount(); + assertThat(firstDataTypeCount).isEqualTo(firstCount); + assertThat(secondDataTypeCount).isEqualTo(secondCount); + } + + @Test + public void testLogItemsBackupFailed_repeatedCalls_recordsTotalItems() { + mLogger = new BackupRestoreEventLogger(BACKUP); + + int firstCount = 10; + int secondCount = 5; + mLogger.logItemsBackupFailed(DATA_TYPE_1, firstCount, /* error */ null); + mLogger.logItemsBackupFailed(DATA_TYPE_1, secondCount, "error"); + + int dataTypeCount = getResultForDataType(mLogger, DATA_TYPE_1).getFailCount(); + assertThat(dataTypeCount).isEqualTo(firstCount + secondCount); + } + + @Test + public void testLogItemsRestoreFailed_repeatedCalls_recordsTotalItems() { + mLogger = new BackupRestoreEventLogger(RESTORE); + + int firstCount = 10; + int secondCount = 5; + mLogger.logItemsRestoreFailed(DATA_TYPE_1, firstCount, /* error */ null); + mLogger.logItemsRestoreFailed(DATA_TYPE_1, secondCount, "error"); + + int dataTypeCount = getResultForDataType(mLogger, DATA_TYPE_1).getFailCount(); + assertThat(dataTypeCount).isEqualTo(firstCount + secondCount); + } + + @Test + public void testLogItemsBackupFailed_multipleErrors_recordsEachError() { + mLogger = new BackupRestoreEventLogger(BACKUP); + + int firstCount = 10; + int secondCount = 5; + mLogger.logItemsBackupFailed(DATA_TYPE_1, firstCount, ERROR_1); + mLogger.logItemsBackupFailed(DATA_TYPE_1, secondCount, ERROR_2); + + int firstErrorTypeCount = getResultForDataType(mLogger, DATA_TYPE_1) + .getErrors().get(ERROR_1); + int secondErrorTypeCount = getResultForDataType(mLogger, DATA_TYPE_1) + .getErrors().get(ERROR_2); + assertThat(firstErrorTypeCount).isEqualTo(firstCount); + assertThat(secondErrorTypeCount).isEqualTo(secondCount); + } + + @Test + public void testLogItemsRestoreFailed_multipleErrors_recordsEachError() { + mLogger = new BackupRestoreEventLogger(RESTORE); + + int firstCount = 10; + int secondCount = 5; + mLogger.logItemsRestoreFailed(DATA_TYPE_1, firstCount, ERROR_1); + mLogger.logItemsRestoreFailed(DATA_TYPE_1, secondCount, ERROR_2); + + int firstErrorTypeCount = getResultForDataType(mLogger, DATA_TYPE_1) + .getErrors().get(ERROR_1); + int secondErrorTypeCount = getResultForDataType(mLogger, DATA_TYPE_1) + .getErrors().get(ERROR_2); + assertThat(firstErrorTypeCount).isEqualTo(firstCount); + assertThat(secondErrorTypeCount).isEqualTo(secondCount); + } + + private static DataTypeResult getResultForDataType(BackupRestoreEventLogger logger, + @BackupRestoreDataType String dataType) { + Optional<DataTypeResult> result = getResultForDataTypeIfPresent(logger, dataType); + if (result.isEmpty()) { + fail("Failed to find result for data type: " + dataType); + } + return result.get(); + } + + private static Optional<DataTypeResult> getResultForDataTypeIfPresent( + BackupRestoreEventLogger logger, @BackupRestoreDataType String dataType) { + List<DataTypeResult> resultList = logger.getLoggingResults(); + return resultList.stream().filter( + dataTypeResult -> dataTypeResult.getDataType().equals(dataType)).findAny(); + } + + private byte[] getMetaDataHash(String metaData) { + return mHashDigest.digest(metaData.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/core/tests/coretests/src/android/app/backup/OWNERS b/core/tests/coretests/src/android/app/backup/OWNERS new file mode 100644 index 000000000000..53b6c78b3895 --- /dev/null +++ b/core/tests/coretests/src/android/app/backup/OWNERS @@ -0,0 +1 @@ +include /services/backup/OWNERS
\ No newline at end of file diff --git a/core/tests/coretests/src/android/os/BundleMergerTest.java b/core/tests/coretests/src/android/os/BundleMergerTest.java new file mode 100644 index 000000000000..b7012ba66124 --- /dev/null +++ b/core/tests/coretests/src/android/os/BundleMergerTest.java @@ -0,0 +1,408 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.os; + +import static android.os.BundleMerger.STRATEGY_ARRAY_APPEND; +import static android.os.BundleMerger.STRATEGY_ARRAY_LIST_APPEND; +import static android.os.BundleMerger.STRATEGY_BOOLEAN_AND; +import static android.os.BundleMerger.STRATEGY_BOOLEAN_OR; +import static android.os.BundleMerger.STRATEGY_COMPARABLE_MAX; +import static android.os.BundleMerger.STRATEGY_COMPARABLE_MIN; +import static android.os.BundleMerger.STRATEGY_FIRST; +import static android.os.BundleMerger.STRATEGY_LAST; +import static android.os.BundleMerger.STRATEGY_NUMBER_ADD; +import static android.os.BundleMerger.STRATEGY_NUMBER_INCREMENT_FIRST; +import static android.os.BundleMerger.STRATEGY_REJECT; +import static android.os.BundleMerger.merge; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import android.content.Intent; +import android.net.Uri; + +import androidx.test.filters.SmallTest; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +@SmallTest +@RunWith(JUnit4.class) +public class BundleMergerTest { + /** + * Strategies are only applied when there is an actual conflict; in the + * absence of conflict we pick whichever value is defined. + */ + @Test + public void testNoConflict() throws Exception { + for (int strategy = Byte.MIN_VALUE; strategy < Byte.MAX_VALUE; strategy++) { + assertEquals(null, merge(strategy, null, null)); + assertEquals(10, merge(strategy, 10, null)); + assertEquals(20, merge(strategy, null, 20)); + } + } + + /** + * Strategies are only applied to identical data types; if there are mixed + * types we always reject the two conflicting values. + */ + @Test + public void testMixedTypes() throws Exception { + for (int strategy = Byte.MIN_VALUE; strategy < Byte.MAX_VALUE; strategy++) { + final int finalStrategy = strategy; + assertThrows(Exception.class, () -> { + merge(finalStrategy, 10, "foo"); + }); + assertThrows(Exception.class, () -> { + merge(finalStrategy, List.of("foo"), "bar"); + }); + assertThrows(Exception.class, () -> { + merge(finalStrategy, new String[] { "foo" }, "bar"); + }); + assertThrows(Exception.class, () -> { + merge(finalStrategy, Integer.valueOf(10), Long.valueOf(10)); + }); + } + } + + @Test + public void testStrategyReject() throws Exception { + assertEquals(null, merge(STRATEGY_REJECT, 10, 20)); + + // Identical values aren't technically a conflict, so they're passed + // through without being rejected + assertEquals(10, merge(STRATEGY_REJECT, 10, 10)); + assertArrayEquals(new int[] {10}, + (int[]) merge(STRATEGY_REJECT, new int[] {10}, new int[] {10})); + } + + @Test + public void testStrategyFirst() throws Exception { + assertEquals(10, merge(STRATEGY_FIRST, 10, 20)); + } + + @Test + public void testStrategyLast() throws Exception { + assertEquals(20, merge(STRATEGY_LAST, 10, 20)); + } + + @Test + public void testStrategyComparableMin() throws Exception { + assertEquals(10, merge(STRATEGY_COMPARABLE_MIN, 10, 20)); + assertEquals(10, merge(STRATEGY_COMPARABLE_MIN, 20, 10)); + assertEquals("a", merge(STRATEGY_COMPARABLE_MIN, "a", "z")); + assertEquals("a", merge(STRATEGY_COMPARABLE_MIN, "z", "a")); + + assertThrows(Exception.class, () -> { + merge(STRATEGY_COMPARABLE_MIN, new Binder(), new Binder()); + }); + } + + @Test + public void testStrategyComparableMax() throws Exception { + assertEquals(20, merge(STRATEGY_COMPARABLE_MAX, 10, 20)); + assertEquals(20, merge(STRATEGY_COMPARABLE_MAX, 20, 10)); + assertEquals("z", merge(STRATEGY_COMPARABLE_MAX, "a", "z")); + assertEquals("z", merge(STRATEGY_COMPARABLE_MAX, "z", "a")); + + assertThrows(Exception.class, () -> { + merge(STRATEGY_COMPARABLE_MAX, new Binder(), new Binder()); + }); + } + + @Test + public void testStrategyNumberAdd() throws Exception { + assertEquals(30, merge(STRATEGY_NUMBER_ADD, 10, 20)); + assertEquals(30, merge(STRATEGY_NUMBER_ADD, 20, 10)); + assertEquals(30L, merge(STRATEGY_NUMBER_ADD, 10L, 20L)); + assertEquals(30L, merge(STRATEGY_NUMBER_ADD, 20L, 10L)); + + assertThrows(Exception.class, () -> { + merge(STRATEGY_NUMBER_ADD, new Binder(), new Binder()); + }); + } + + @Test + public void testStrategyNumberIncrementFirst() throws Exception { + assertEquals(11, merge(STRATEGY_NUMBER_INCREMENT_FIRST, 10, 20)); + assertEquals(21, merge(STRATEGY_NUMBER_INCREMENT_FIRST, 20, 10)); + assertEquals(11L, merge(STRATEGY_NUMBER_INCREMENT_FIRST, 10L, 20L)); + assertEquals(21L, merge(STRATEGY_NUMBER_INCREMENT_FIRST, 20L, 10L)); + } + + @Test + public void testStrategyBooleanAnd() throws Exception { + assertEquals(false, merge(STRATEGY_BOOLEAN_AND, false, false)); + assertEquals(false, merge(STRATEGY_BOOLEAN_AND, true, false)); + assertEquals(false, merge(STRATEGY_BOOLEAN_AND, false, true)); + assertEquals(true, merge(STRATEGY_BOOLEAN_AND, true, true)); + + assertThrows(Exception.class, () -> { + merge(STRATEGY_BOOLEAN_AND, "True!", "False?"); + }); + } + + @Test + public void testStrategyBooleanOr() throws Exception { + assertEquals(false, merge(STRATEGY_BOOLEAN_OR, false, false)); + assertEquals(true, merge(STRATEGY_BOOLEAN_OR, true, false)); + assertEquals(true, merge(STRATEGY_BOOLEAN_OR, false, true)); + assertEquals(true, merge(STRATEGY_BOOLEAN_OR, true, true)); + + assertThrows(Exception.class, () -> { + merge(STRATEGY_BOOLEAN_OR, "True!", "False?"); + }); + } + + @Test + public void testStrategyArrayAppend() throws Exception { + assertArrayEquals(new int[] {}, + (int[]) merge(STRATEGY_ARRAY_APPEND, new int[] {}, new int[] {})); + assertArrayEquals(new int[] {10}, + (int[]) merge(STRATEGY_ARRAY_APPEND, new int[] {10}, new int[] {})); + assertArrayEquals(new int[] {20}, + (int[]) merge(STRATEGY_ARRAY_APPEND, new int[] {}, new int[] {20})); + assertArrayEquals(new int[] {10, 20}, + (int[]) merge(STRATEGY_ARRAY_APPEND, new int[] {10}, new int[] {20})); + assertArrayEquals(new int[] {10, 30, 20, 40}, + (int[]) merge(STRATEGY_ARRAY_APPEND, new int[] {10, 30}, new int[] {20, 40})); + assertArrayEquals(new String[] {"a", "b"}, + (String[]) merge(STRATEGY_ARRAY_APPEND, new String[] {"a"}, new String[] {"b"})); + + assertThrows(Exception.class, () -> { + merge(STRATEGY_ARRAY_APPEND, 10, 20); + }); + } + + @Test + public void testStrategyArrayListAppend() throws Exception { + assertEquals(arrayListOf(), + merge(STRATEGY_ARRAY_LIST_APPEND, arrayListOf(), arrayListOf())); + assertEquals(arrayListOf(10), + merge(STRATEGY_ARRAY_LIST_APPEND, arrayListOf(10), arrayListOf())); + assertEquals(arrayListOf(20), + merge(STRATEGY_ARRAY_LIST_APPEND, arrayListOf(), arrayListOf(20))); + assertEquals(arrayListOf(10, 20), + merge(STRATEGY_ARRAY_LIST_APPEND, arrayListOf(10), arrayListOf(20))); + assertEquals(arrayListOf(10, 30, 20, 40), + merge(STRATEGY_ARRAY_LIST_APPEND, arrayListOf(10, 30), arrayListOf(20, 40))); + assertEquals(arrayListOf("a", "b"), + merge(STRATEGY_ARRAY_LIST_APPEND, arrayListOf("a"), arrayListOf("b"))); + + assertThrows(Exception.class, () -> { + merge(STRATEGY_ARRAY_LIST_APPEND, 10, 20); + }); + } + + @Test + public void testMerge_Simple() throws Exception { + final BundleMerger merger = new BundleMerger(); + final Bundle probe = new Bundle(); + probe.putInt(Intent.EXTRA_INDEX, 42); + + assertEquals(null, merger.merge(null, null)); + assertEquals(probe.keySet(), merger.merge(probe, null).keySet()); + assertEquals(probe.keySet(), merger.merge(null, probe).keySet()); + assertEquals(probe.keySet(), merger.merge(probe, probe).keySet()); + } + + /** + * Verify that we can merge parcelables present in the base classpath, since + * everyone on the device will be able to unpack them. + */ + @Test + public void testMerge_Parcelable_BCP() throws Exception { + final BundleMerger merger = new BundleMerger(); + merger.setMergeStrategy(Intent.EXTRA_STREAM, STRATEGY_COMPARABLE_MIN); + + Bundle a = new Bundle(); + a.putParcelable(Intent.EXTRA_STREAM, Uri.parse("http://example.com")); + a = parcelAndUnparcel(a); + + Bundle b = new Bundle(); + b.putParcelable(Intent.EXTRA_STREAM, Uri.parse("http://example.net")); + b = parcelAndUnparcel(b); + + assertEquals(Uri.parse("http://example.com"), + merger.merge(a, b).getParcelable(Intent.EXTRA_STREAM, Uri.class)); + assertEquals(Uri.parse("http://example.com"), + merger.merge(b, a).getParcelable(Intent.EXTRA_STREAM, Uri.class)); + } + + /** + * Verify that we tiptoe around custom parcelables while still merging other + * known data types. Custom parcelables aren't in the base classpath, so not + * everyone on the device will be able to unpack them. + */ + @Test + public void testMerge_Parcelable_Custom() throws Exception { + final BundleMerger merger = new BundleMerger(); + merger.setMergeStrategy(Intent.EXTRA_INDEX, STRATEGY_NUMBER_ADD); + + Bundle a = new Bundle(); + a.putInt(Intent.EXTRA_INDEX, 10); + a.putString(Intent.EXTRA_CC, "foo@bar.com"); + a.putParcelable(Intent.EXTRA_SUBJECT, new ExplodingParcelable()); + a = parcelAndUnparcel(a); + + Bundle b = new Bundle(); + b.putInt(Intent.EXTRA_INDEX, 20); + a.putString(Intent.EXTRA_BCC, "foo@baz.com"); + b.putParcelable(Intent.EXTRA_STREAM, new ExplodingParcelable()); + b = parcelAndUnparcel(b); + + Bundle ab = merger.merge(a, b); + assertEquals(Set.of(Intent.EXTRA_INDEX, Intent.EXTRA_CC, Intent.EXTRA_BCC, + Intent.EXTRA_SUBJECT, Intent.EXTRA_STREAM), ab.keySet()); + assertEquals(30, ab.getInt(Intent.EXTRA_INDEX)); + assertEquals("foo@bar.com", ab.getString(Intent.EXTRA_CC)); + assertEquals("foo@baz.com", ab.getString(Intent.EXTRA_BCC)); + + // And finally, make sure that if we try unpacking one of our custom + // values that we actually explode + assertThrows(BadParcelableException.class, () -> { + ab.getParcelable(Intent.EXTRA_SUBJECT, ExplodingParcelable.class); + }); + assertThrows(BadParcelableException.class, () -> { + ab.getParcelable(Intent.EXTRA_STREAM, ExplodingParcelable.class); + }); + } + + @Test + public void testMerge_PackageChanged() throws Exception { + final BundleMerger merger = new BundleMerger(); + merger.setMergeStrategy(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST, STRATEGY_ARRAY_APPEND); + + final Bundle first = new Bundle(); + first.putInt(Intent.EXTRA_UID, 10001); + first.putStringArray(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST, new String[] { + "com.example.Foo", + }); + + final Bundle second = new Bundle(); + second.putInt(Intent.EXTRA_UID, 10001); + second.putStringArray(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST, new String[] { + "com.example.Bar", + "com.example.Baz", + }); + + final Bundle res = merger.merge(first, second); + assertEquals(10001, res.getInt(Intent.EXTRA_UID)); + assertArrayEquals(new String[] { + "com.example.Foo", "com.example.Bar", "com.example.Baz", + }, res.getStringArray(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST)); + } + + /** + * Each event in isolation reports "zero events dropped", but if we need to + * merge them together, then we start incrementing. + */ + @Test + public void testMerge_DropBox() throws Exception { + final BundleMerger merger = new BundleMerger(); + merger.setMergeStrategy(DropBoxManager.EXTRA_TIME, + STRATEGY_COMPARABLE_MAX); + merger.setMergeStrategy(DropBoxManager.EXTRA_DROPPED_COUNT, + STRATEGY_NUMBER_INCREMENT_FIRST); + + final long now = System.currentTimeMillis(); + final Bundle a = new Bundle(); + a.putString(DropBoxManager.EXTRA_TAG, "system_server_strictmode"); + a.putLong(DropBoxManager.EXTRA_TIME, now); + a.putInt(DropBoxManager.EXTRA_DROPPED_COUNT, 0); + + final Bundle b = new Bundle(); + b.putString(DropBoxManager.EXTRA_TAG, "system_server_strictmode"); + b.putLong(DropBoxManager.EXTRA_TIME, now + 1000); + b.putInt(DropBoxManager.EXTRA_DROPPED_COUNT, 0); + + final Bundle c = new Bundle(); + c.putString(DropBoxManager.EXTRA_TAG, "system_server_strictmode"); + c.putLong(DropBoxManager.EXTRA_TIME, now + 2000); + c.putInt(DropBoxManager.EXTRA_DROPPED_COUNT, 0); + + final Bundle ab = merger.merge(a, b); + assertEquals("system_server_strictmode", ab.getString(DropBoxManager.EXTRA_TAG)); + assertEquals(now + 1000, ab.getLong(DropBoxManager.EXTRA_TIME)); + assertEquals(1, ab.getInt(DropBoxManager.EXTRA_DROPPED_COUNT)); + + final Bundle abc = merger.merge(ab, c); + assertEquals("system_server_strictmode", abc.getString(DropBoxManager.EXTRA_TAG)); + assertEquals(now + 2000, abc.getLong(DropBoxManager.EXTRA_TIME)); + assertEquals(2, abc.getInt(DropBoxManager.EXTRA_DROPPED_COUNT)); + } + + private static ArrayList<Object> arrayListOf(Object... values) { + final ArrayList<Object> res = new ArrayList<>(values.length); + for (Object value : values) { + res.add(value); + } + return res; + } + + private static Bundle parcelAndUnparcel(Bundle input) { + final Parcel parcel = Parcel.obtain(); + try { + input.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + return Bundle.CREATOR.createFromParcel(parcel); + } finally { + parcel.recycle(); + } + } + + /** + * Object that only offers to parcel itself; if something tries unparceling + * it, it will "explode" by throwing an exception. + * <p> + * Useful for verifying interactions that must leave unknown data in a + * parceled state. + */ + public static class ExplodingParcelable implements Parcelable { + public ExplodingParcelable() { + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeInt(42); + } + + public static final Creator<ExplodingParcelable> CREATOR = + new Creator<ExplodingParcelable>() { + @Override + public ExplodingParcelable createFromParcel(Parcel in) { + throw new BadParcelableException("exploding!"); + } + + @Override + public ExplodingParcelable[] newArray(int size) { + throw new BadParcelableException("exploding!"); + } + }; + } +} diff --git a/core/tests/coretests/src/android/os/ParcelTest.java b/core/tests/coretests/src/android/os/ParcelTest.java index fdd278b9c621..e2fe87b4cfe3 100644 --- a/core/tests/coretests/src/android/os/ParcelTest.java +++ b/core/tests/coretests/src/android/os/ParcelTest.java @@ -37,6 +37,13 @@ public class ParcelTest { private static final String INTERFACE_TOKEN_2 = "Another IBinder interface token"; @Test + public void testIsForRpc() { + Parcel p = Parcel.obtain(); + assertEquals(false, p.isForRpc()); + p.recycle(); + } + + @Test public void testCallingWorkSourceUidAfterWrite() { Parcel p = Parcel.obtain(); // Method does not throw if replaceCallingWorkSourceUid is called before requests headers diff --git a/core/tests/coretests/src/android/text/method/BackspaceTest.java b/core/tests/coretests/src/android/text/method/BackspaceTest.java index ddae652cec05..19c2c6153558 100644 --- a/core/tests/coretests/src/android/text/method/BackspaceTest.java +++ b/core/tests/coretests/src/android/text/method/BackspaceTest.java @@ -193,11 +193,15 @@ public class BackspaceTest { backspace(state, 0); state.assertEquals("|"); - // Emoji modifier can be appended to the first emoji. + // Emoji modifier can be appended to each emoji. state.setByString("U+1F469 U+1F3FB U+200D U+1F4BC |"); backspace(state, 0); state.assertEquals("|"); + state.setByString("U+1F468 U+1F3FF U+200D U+2764 U+FE0F U+200D U+1F468 U+1F3FB |"); + backspace(state, 0); + state.assertEquals("|"); + // End with ZERO WIDTH JOINER state.setByString("U+1F441 U+200D |"); backspace(state, 0); diff --git a/core/tests/coretests/src/android/view/InsetsControllerTest.java b/core/tests/coretests/src/android/view/InsetsControllerTest.java index cc68fcee9f32..5e12313e4136 100644 --- a/core/tests/coretests/src/android/view/InsetsControllerTest.java +++ b/core/tests/coretests/src/android/view/InsetsControllerTest.java @@ -31,9 +31,12 @@ import static android.view.InsetsState.ITYPE_NAVIGATION_BAR; import static android.view.InsetsState.ITYPE_STATUS_BAR; import static android.view.InsetsState.LAST_TYPE; import static android.view.ViewRootImpl.CAPTION_ON_SHELL; +import static android.view.WindowInsets.Type.all; +import static android.view.WindowInsets.Type.defaultVisible; import static android.view.WindowInsets.Type.ime; import static android.view.WindowInsets.Type.navigationBars; import static android.view.WindowInsets.Type.statusBars; +import static android.view.WindowInsets.Type.systemBars; import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; @@ -63,7 +66,7 @@ import android.os.CancellationSignal; import android.platform.test.annotations.Presubmit; import android.view.InsetsState.InternalInsetsType; import android.view.SurfaceControl.Transaction; -import android.view.WindowInsets.Type; +import android.view.WindowInsets.Type.InsetsType; import android.view.WindowInsetsController.OnControllableInsetsChangedListener; import android.view.WindowManager.BadTokenException; import android.view.WindowManager.LayoutParams; @@ -245,7 +248,7 @@ public class InsetsControllerTest { mController.setSystemDrivenInsetsAnimationLoggingListener(loggingListener); mController.getSourceConsumer(ITYPE_IME).onWindowFocusGained(true); // since there is no focused view, forcefully make IME visible. - mController.show(Type.ime(), true /* fromIme */); + mController.show(ime(), true /* fromIme */); verify(loggingListener).onReady(notNull(), anyInt()); }); } @@ -260,16 +263,16 @@ public class InsetsControllerTest { InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { mController.getSourceConsumer(ITYPE_IME).onWindowFocusGained(true); // since there is no focused view, forcefully make IME visible. - mController.show(Type.ime(), true /* fromIme */); - mController.show(Type.all()); + mController.show(ime(), true /* fromIme */); + mController.show(all()); // quickly jump to final state by cancelling it. mController.cancelExistingAnimations(); assertTrue(mController.getSourceConsumer(navBar.getType()).isRequestedVisible()); assertTrue(mController.getSourceConsumer(statusBar.getType()).isRequestedVisible()); assertTrue(mController.getSourceConsumer(ime.getType()).isRequestedVisible()); - mController.hide(Type.ime(), true /* fromIme */); - mController.hide(Type.all()); + mController.hide(ime(), true /* fromIme */); + mController.hide(all()); mController.cancelExistingAnimations(); assertFalse(mController.getSourceConsumer(navBar.getType()).isRequestedVisible()); assertFalse(mController.getSourceConsumer(statusBar.getType()).isRequestedVisible()); @@ -285,10 +288,10 @@ public class InsetsControllerTest { mController.onControlsChanged(new InsetsSourceControl[] { ime }); InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { mController.getSourceConsumer(ITYPE_IME).onWindowFocusGained(true); - mController.show(Type.ime(), true /* fromIme */); + mController.show(ime(), true /* fromIme */); mController.cancelExistingAnimations(); assertTrue(mController.getSourceConsumer(ime.getType()).isRequestedVisible()); - mController.hide(Type.ime(), true /* fromIme */); + mController.hide(ime(), true /* fromIme */); mController.cancelExistingAnimations(); assertFalse(mController.getSourceConsumer(ime.getType()).isRequestedVisible()); mController.getSourceConsumer(ITYPE_IME).onWindowFocusLost(); @@ -304,7 +307,7 @@ public class InsetsControllerTest { InsetsSourceControl ime = controls[2]; InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { - int types = Type.navigationBars() | Type.systemBars(); + int types = navigationBars() | systemBars(); // test hide select types. mController.hide(types); assertEquals(ANIMATION_TYPE_HIDE, mController.getAnimationType(ITYPE_NAVIGATION_BAR)); @@ -336,7 +339,7 @@ public class InsetsControllerTest { InsetsSourceControl ime = controls[2]; InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { - int types = Type.navigationBars() | Type.systemBars(); + int types = navigationBars() | systemBars(); // test show select types. mController.show(types); mController.cancelExistingAnimations(); @@ -345,21 +348,21 @@ public class InsetsControllerTest { assertFalse(mController.getSourceConsumer(ime.getType()).isRequestedVisible()); // test hide all - mController.hide(Type.all()); + mController.hide(all()); mController.cancelExistingAnimations(); assertFalse(mController.getSourceConsumer(navBar.getType()).isRequestedVisible()); assertFalse(mController.getSourceConsumer(statusBar.getType()).isRequestedVisible()); assertFalse(mController.getSourceConsumer(ime.getType()).isRequestedVisible()); // test single show - mController.show(Type.navigationBars()); + mController.show(navigationBars()); mController.cancelExistingAnimations(); assertTrue(mController.getSourceConsumer(navBar.getType()).isRequestedVisible()); assertFalse(mController.getSourceConsumer(statusBar.getType()).isRequestedVisible()); assertFalse(mController.getSourceConsumer(ime.getType()).isRequestedVisible()); // test single hide - mController.hide(Type.navigationBars()); + mController.hide(navigationBars()); assertFalse(mController.getSourceConsumer(navBar.getType()).isRequestedVisible()); assertFalse(mController.getSourceConsumer(statusBar.getType()).isRequestedVisible()); assertFalse(mController.getSourceConsumer(ime.getType()).isRequestedVisible()); @@ -377,8 +380,8 @@ public class InsetsControllerTest { InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { // start two animations and see if previous is cancelled and final state is reached. - mController.hide(Type.navigationBars()); - mController.hide(Type.systemBars()); + mController.hide(navigationBars()); + mController.hide(systemBars()); assertEquals(ANIMATION_TYPE_HIDE, mController.getAnimationType(ITYPE_NAVIGATION_BAR)); assertEquals(ANIMATION_TYPE_HIDE, mController.getAnimationType(ITYPE_STATUS_BAR)); mController.cancelExistingAnimations(); @@ -386,8 +389,8 @@ public class InsetsControllerTest { assertFalse(mController.getSourceConsumer(statusBar.getType()).isRequestedVisible()); assertFalse(mController.getSourceConsumer(ime.getType()).isRequestedVisible()); - mController.show(Type.navigationBars()); - mController.show(Type.systemBars()); + mController.show(navigationBars()); + mController.show(systemBars()); assertEquals(ANIMATION_TYPE_SHOW, mController.getAnimationType(ITYPE_NAVIGATION_BAR)); assertEquals(ANIMATION_TYPE_SHOW, mController.getAnimationType(ITYPE_STATUS_BAR)); mController.cancelExistingAnimations(); @@ -395,10 +398,10 @@ public class InsetsControllerTest { assertTrue(mController.getSourceConsumer(statusBar.getType()).isRequestedVisible()); assertFalse(mController.getSourceConsumer(ime.getType()).isRequestedVisible()); - int types = Type.navigationBars() | Type.systemBars(); + int types = navigationBars() | systemBars(); // show two at a time and hide one by one. mController.show(types); - mController.hide(Type.navigationBars()); + mController.hide(navigationBars()); assertEquals(ANIMATION_TYPE_HIDE, mController.getAnimationType(ITYPE_NAVIGATION_BAR)); assertEquals(ANIMATION_TYPE_NONE, mController.getAnimationType(ITYPE_STATUS_BAR)); mController.cancelExistingAnimations(); @@ -406,7 +409,7 @@ public class InsetsControllerTest { assertTrue(mController.getSourceConsumer(statusBar.getType()).isRequestedVisible()); assertFalse(mController.getSourceConsumer(ime.getType()).isRequestedVisible()); - mController.hide(Type.systemBars()); + mController.hide(systemBars()); assertEquals(ANIMATION_TYPE_NONE, mController.getAnimationType(ITYPE_NAVIGATION_BAR)); assertEquals(ANIMATION_TYPE_HIDE, mController.getAnimationType(ITYPE_STATUS_BAR)); mController.cancelExistingAnimations(); @@ -425,16 +428,16 @@ public class InsetsControllerTest { InsetsSourceControl ime = controls[2]; InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { - int types = Type.navigationBars() | Type.systemBars(); + int types = navigationBars() | systemBars(); // show two at a time and hide one by one. mController.show(types); - mController.hide(Type.navigationBars()); + mController.hide(navigationBars()); mController.cancelExistingAnimations(); assertFalse(mController.getSourceConsumer(navBar.getType()).isRequestedVisible()); assertTrue(mController.getSourceConsumer(statusBar.getType()).isRequestedVisible()); assertFalse(mController.getSourceConsumer(ime.getType()).isRequestedVisible()); - mController.hide(Type.systemBars()); + mController.hide(systemBars()); mController.cancelExistingAnimations(); assertFalse(mController.getSourceConsumer(navBar.getType()).isRequestedVisible()); assertFalse(mController.getSourceConsumer(statusBar.getType()).isRequestedVisible()); @@ -448,7 +451,7 @@ public class InsetsControllerTest { mController.onControlsChanged(createSingletonControl(ITYPE_STATUS_BAR)); InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { - mController.hide(Type.statusBars()); + mController.hide(statusBars()); mController.cancelExistingAnimations(); assertFalse(mController.getSourceConsumer(ITYPE_STATUS_BAR).isRequestedVisible()); assertFalse(mController.getState().getSource(ITYPE_STATUS_BAR).isVisible()); @@ -689,7 +692,7 @@ public class InsetsControllerTest { public void testResizeAnimation_insetsTypes() { for (@InternalInsetsType int type = FIRST_TYPE; type <= LAST_TYPE; type++) { final @AnimationType int expectedAnimationType = - (InsetsState.toPublicType(type) & Type.systemBars()) != 0 + (InsetsState.toPublicType(type) & systemBars()) != 0 ? ANIMATION_TYPE_RESIZE : ANIMATION_TYPE_NONE; doTestResizeAnimation_insetsTypes(type, expectedAnimationType); @@ -824,15 +827,13 @@ public class InsetsControllerTest { @Test public void testRequestedState() { InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { - final InsetsVisibilities request = mTestHost.getRequestedVisibilities(); - mController.hide(statusBars() | navigationBars()); - assertFalse(request.getVisibility(ITYPE_STATUS_BAR)); - assertFalse(request.getVisibility(ITYPE_NAVIGATION_BAR)); + assertFalse(mTestHost.isRequestedVisible(statusBars())); + assertFalse(mTestHost.isRequestedVisible(navigationBars())); mController.show(statusBars() | navigationBars()); - assertTrue(request.getVisibility(ITYPE_STATUS_BAR)); - assertTrue(request.getVisibility(ITYPE_NAVIGATION_BAR)); + assertTrue(mTestHost.isRequestedVisible(statusBars())); + assertTrue(mTestHost.isRequestedVisible(navigationBars())); }); } @@ -981,20 +982,20 @@ public class InsetsControllerTest { public static class TestHost extends ViewRootInsetsControllerHost { - private final InsetsVisibilities mRequestedVisibilities = new InsetsVisibilities(); + private @InsetsType int mRequestedVisibleTypes = defaultVisible(); TestHost(ViewRootImpl viewRoot) { super(viewRoot); } @Override - public void updateRequestedVisibilities(InsetsVisibilities visibilities) { - mRequestedVisibilities.set(visibilities); - super.updateRequestedVisibilities(visibilities); + public void updateRequestedVisibleTypes(@InsetsType int requestedVisibleTypes) { + mRequestedVisibleTypes = requestedVisibleTypes; + super.updateRequestedVisibleTypes(requestedVisibleTypes); } - public InsetsVisibilities getRequestedVisibilities() { - return mRequestedVisibilities; + public boolean isRequestedVisible(@InsetsType int types) { + return (mRequestedVisibleTypes & types) != 0; } } } diff --git a/core/tests/coretests/src/com/android/internal/security/ContentSignerWrapper.java b/core/tests/coretests/src/com/android/internal/security/ContentSignerWrapper.java new file mode 100644 index 000000000000..0254afee35dd --- /dev/null +++ b/core/tests/coretests/src/com/android/internal/security/ContentSignerWrapper.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.security; + +import org.bouncycastle.asn1.x509.AlgorithmIdentifier; +import org.bouncycastle.operator.ContentSigner; + +import java.io.OutputStream; + +/** A wrapper class of ContentSigner */ +class ContentSignerWrapper implements ContentSigner { + private final ContentSigner mSigner; + + ContentSignerWrapper(ContentSigner wrapped) { + mSigner = wrapped; + } + + @Override + public AlgorithmIdentifier getAlgorithmIdentifier() { + return mSigner.getAlgorithmIdentifier(); + } + + @Override + public OutputStream getOutputStream() { + return mSigner.getOutputStream(); + } + + @Override + public byte[] getSignature() { + return mSigner.getSignature(); + } +} diff --git a/core/tests/coretests/src/com/android/internal/security/VerityUtilsTest.java b/core/tests/coretests/src/com/android/internal/security/VerityUtilsTest.java new file mode 100644 index 000000000000..151365481f20 --- /dev/null +++ b/core/tests/coretests/src/com/android/internal/security/VerityUtilsTest.java @@ -0,0 +1,355 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.security; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.platform.test.annotations.Presubmit; + +import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; + +import com.android.frameworks.coretests.R; + +import org.bouncycastle.asn1.ASN1Encoding; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.nist.NISTObjectIdentifiers; +import org.bouncycastle.asn1.oiw.OIWObjectIdentifiers; +import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.AlgorithmIdentifier; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cms.CMSException; +import org.bouncycastle.cms.CMSProcessableByteArray; +import org.bouncycastle.cms.CMSSignedData; +import org.bouncycastle.cms.CMSSignedDataGenerator; +import org.bouncycastle.cms.SignerInfoGenerator; +import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.DigestCalculator; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.bc.BcDigestCalculatorProvider; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.time.Duration; +import java.time.Instant; +import java.util.Arrays; +import java.util.Date; +import java.util.HexFormat; + +@Presubmit +@SmallTest +@RunWith(AndroidJUnit4.class) +public class VerityUtilsTest { + private static final byte[] SAMPLE_DIGEST = "12345678901234567890123456789012".getBytes(); + private static final byte[] FORMATTED_SAMPLE_DIGEST = toFormattedDigest(SAMPLE_DIGEST); + + KeyPair mKeyPair; + ContentSigner mContentSigner; + X509CertificateHolder mCertificateHolder; + byte[] mCertificateDerEncoded; + + @Before + public void setUp() throws Exception { + mKeyPair = KeyPairGenerator.getInstance("RSA").generateKeyPair(); + mContentSigner = newFsverityContentSigner(mKeyPair.getPrivate()); + mCertificateHolder = + newX509CertificateHolder(mContentSigner, mKeyPair.getPublic(), "Someone"); + mCertificateDerEncoded = mCertificateHolder.getEncoded(); + } + + @Test + public void testOnlyAcceptCorrectDigest() throws Exception { + byte[] pkcs7Signature = + generatePkcs7Signature(mContentSigner, mCertificateHolder, FORMATTED_SAMPLE_DIGEST); + + byte[] anotherDigest = Arrays.copyOf(SAMPLE_DIGEST, SAMPLE_DIGEST.length); + anotherDigest[0] ^= (byte) 1; + + assertTrue(verifySignature(pkcs7Signature, SAMPLE_DIGEST, mCertificateDerEncoded)); + assertFalse(verifySignature(pkcs7Signature, anotherDigest, mCertificateDerEncoded)); + } + + @Test + public void testDigestWithWrongSize() throws Exception { + byte[] pkcs7Signature = + generatePkcs7Signature(mContentSigner, mCertificateHolder, FORMATTED_SAMPLE_DIGEST); + assertTrue(verifySignature(pkcs7Signature, SAMPLE_DIGEST, mCertificateDerEncoded)); + + byte[] digestTooShort = Arrays.copyOfRange(SAMPLE_DIGEST, 0, SAMPLE_DIGEST.length - 1); + assertFalse(verifySignature(pkcs7Signature, digestTooShort, mCertificateDerEncoded)); + + byte[] digestTooLong = Arrays.copyOfRange(SAMPLE_DIGEST, 0, SAMPLE_DIGEST.length + 1); + assertFalse(verifySignature(pkcs7Signature, digestTooLong, mCertificateDerEncoded)); + } + + @Test + public void testOnlyAcceptGoodSignature() throws Exception { + byte[] pkcs7Signature = + generatePkcs7Signature(mContentSigner, mCertificateHolder, FORMATTED_SAMPLE_DIGEST); + + byte[] anotherDigest = Arrays.copyOf(SAMPLE_DIGEST, SAMPLE_DIGEST.length); + anotherDigest[0] ^= (byte) 1; + byte[] anotherPkcs7Signature = + generatePkcs7Signature( + mContentSigner, mCertificateHolder, toFormattedDigest(anotherDigest)); + + assertTrue(verifySignature(pkcs7Signature, SAMPLE_DIGEST, mCertificateDerEncoded)); + assertFalse(verifySignature(anotherPkcs7Signature, SAMPLE_DIGEST, mCertificateDerEncoded)); + } + + @Test + public void testOnlyValidCertCanVerify() throws Exception { + byte[] pkcs7Signature = + generatePkcs7Signature(mContentSigner, mCertificateHolder, FORMATTED_SAMPLE_DIGEST); + + var wrongKeyPair = KeyPairGenerator.getInstance("RSA").generateKeyPair(); + var wrongContentSigner = newFsverityContentSigner(wrongKeyPair.getPrivate()); + var wrongCertificateHolder = + newX509CertificateHolder(wrongContentSigner, wrongKeyPair.getPublic(), "Not Me"); + byte[] wrongCertificateDerEncoded = wrongCertificateHolder.getEncoded(); + + assertFalse(verifySignature(pkcs7Signature, SAMPLE_DIGEST, wrongCertificateDerEncoded)); + } + + @Test + public void testRejectSignatureWithContent() throws Exception { + CMSSignedDataGenerator generator = + newFsveritySignedDataGenerator(mContentSigner, mCertificateHolder); + byte[] pkcs7SignatureNonDetached = + generatePkcs7SignatureInternal( + generator, FORMATTED_SAMPLE_DIGEST, /* encapsulate */ true); + + assertFalse( + verifySignature(pkcs7SignatureNonDetached, SAMPLE_DIGEST, mCertificateDerEncoded)); + } + + @Test + public void testRejectSignatureWithCertificate() throws Exception { + CMSSignedDataGenerator generator = + newFsveritySignedDataGenerator(mContentSigner, mCertificateHolder); + generator.addCertificate(mCertificateHolder); + byte[] pkcs7Signature = + generatePkcs7SignatureInternal( + generator, FORMATTED_SAMPLE_DIGEST, /* encapsulate */ false); + + assertFalse( + verifySignature(pkcs7Signature, SAMPLE_DIGEST, mCertificateDerEncoded)); + } + + @Ignore("No easy way to construct test data") + @Test + public void testRejectSignatureWithCRL() throws Exception { + CMSSignedDataGenerator generator = + newFsveritySignedDataGenerator(mContentSigner, mCertificateHolder); + + // The current bouncycastle version does not have an easy way to generate a CRL. + // TODO: enable the test once this is doable, e.g. with X509v2CRLBuilder. + // generator.addCRL(new X509CRLHolder(CertificateList.getInstance(new DERSequence(...)))); + byte[] pkcs7Signature = + generatePkcs7SignatureInternal( + generator, FORMATTED_SAMPLE_DIGEST, /* encapsulate */ false); + + assertFalse( + verifySignature(pkcs7Signature, SAMPLE_DIGEST, mCertificateDerEncoded)); + } + + @Test + public void testRejectUnsupportedSignatureAlgorithms() throws Exception { + var contentSigner = newFsverityContentSigner(mKeyPair.getPrivate(), "MD5withRSA", null); + var certificateHolder = + newX509CertificateHolder(contentSigner, mKeyPair.getPublic(), "Someone"); + byte[] pkcs7Signature = + generatePkcs7Signature(contentSigner, certificateHolder, FORMATTED_SAMPLE_DIGEST); + + assertFalse(verifySignature(pkcs7Signature, SAMPLE_DIGEST, certificateHolder.getEncoded())); + } + + @Test + public void testRejectUnsupportedDigestAlgorithm() throws Exception { + CMSSignedDataGenerator generator = new CMSSignedDataGenerator(); + generator.addSignerInfoGenerator( + newSignerInfoGenerator( + mContentSigner, + mCertificateHolder, + OIWObjectIdentifiers.idSHA1, + true)); // directSignature + byte[] pkcs7Signature = + generatePkcs7SignatureInternal( + generator, FORMATTED_SAMPLE_DIGEST, /* encapsulate */ false); + + assertFalse(verifySignature(pkcs7Signature, SAMPLE_DIGEST, mCertificateDerEncoded)); + } + + @Test + public void testRejectAnySignerInfoAttributes() throws Exception { + var generator = new CMSSignedDataGenerator(); + generator.addSignerInfoGenerator( + newSignerInfoGenerator( + mContentSigner, + mCertificateHolder, + NISTObjectIdentifiers.id_sha256, + false)); // directSignature + byte[] pkcs7Signature = + generatePkcs7SignatureInternal( + generator, FORMATTED_SAMPLE_DIGEST, /* encapsulate */ false); + + assertFalse(verifySignature(pkcs7Signature, SAMPLE_DIGEST, mCertificateDerEncoded)); + } + + @Test + public void testSignatureGeneratedExternally() throws Exception { + var context = InstrumentationRegistry.getInstrumentation().getContext(); + byte[] cert = getClass().getClassLoader().getResourceAsStream("ApkVerityTestCert.der") + .readAllBytes(); + // The signature is generated by: + // fsverity sign <(echo -n fs-verity) fsverity_sig --key=ApkVerityTestKey.pem \ + // --cert=ApkVerityTestCert.pem + byte[] sig = context.getResources().openRawResource(R.raw.fsverity_sig).readAllBytes(); + // The fs-verity digest is generated by: + // fsverity digest --compact <(echo -n fs-verity) + byte[] digest = HexFormat.of().parseHex( + "3d248ca542a24fc62d1c43b916eae5016878e2533c88238480b26128a1f1af95"); + + assertTrue(verifySignature(sig, digest, cert)); + } + + private static boolean verifySignature( + byte[] pkcs7Signature, byte[] fsverityDigest, byte[] certificateDerEncoded) { + return VerityUtils.verifyPkcs7DetachedSignature( + pkcs7Signature, fsverityDigest, new ByteArrayInputStream(certificateDerEncoded)); + } + + private static byte[] toFormattedDigest(byte[] digest) { + return VerityUtils.toFormattedDigest(digest); + } + + private static byte[] generatePkcs7Signature( + ContentSigner contentSigner, X509CertificateHolder certificateHolder, byte[] signedData) + throws IOException, CMSException, OperatorCreationException { + CMSSignedDataGenerator generator = + newFsveritySignedDataGenerator(contentSigner, certificateHolder); + return generatePkcs7SignatureInternal(generator, signedData, /* encapsulate */ false); + } + + private static byte[] generatePkcs7SignatureInternal( + CMSSignedDataGenerator generator, byte[] signedData, boolean encapsulate) + throws IOException, CMSException, OperatorCreationException { + CMSSignedData cmsSignedData = + generator.generate(new CMSProcessableByteArray(signedData), encapsulate); + return cmsSignedData.toASN1Structure().getEncoded(ASN1Encoding.DL); + } + + private static CMSSignedDataGenerator newFsveritySignedDataGenerator( + ContentSigner contentSigner, X509CertificateHolder certificateHolder) + throws IOException, CMSException, OperatorCreationException { + var generator = new CMSSignedDataGenerator(); + generator.addSignerInfoGenerator( + newSignerInfoGenerator( + contentSigner, + certificateHolder, + NISTObjectIdentifiers.id_sha256, + true)); // directSignature + return generator; + } + + private static SignerInfoGenerator newSignerInfoGenerator( + ContentSigner contentSigner, + X509CertificateHolder certificateHolder, + ASN1ObjectIdentifier digestAlgorithmId, + boolean directSignature) + throws IOException, CMSException, OperatorCreationException { + var provider = + new BcDigestCalculatorProvider() { + /** + * Allow the caller to override the digest algorithm, especially when the + * default does not work (i.e. BcDigestCalculatorProvider could return null). + * + * <p>For example, the current fs-verity signature has to use rsaEncryption for + * the signature algorithm, but BcDigestCalculatorProvider will return null, + * thus we need a way to override. + * + * <p>TODO: After bouncycastle 1.70, we can remove this override and just use + * {@code JcaSignerInfoGeneratorBuilder#setContentDigest}. + */ + @Override + public DigestCalculator get(AlgorithmIdentifier algorithm) + throws OperatorCreationException { + return super.get(new AlgorithmIdentifier(digestAlgorithmId)); + } + }; + var builder = + new JcaSignerInfoGeneratorBuilder(provider).setDirectSignature(directSignature); + return builder.build(contentSigner, certificateHolder); + } + + private static ContentSigner newFsverityContentSigner(PrivateKey privateKey) + throws OperatorCreationException { + // fs-verity expects the signature to have rsaEncryption as the exact algorithm, so + // override the default. + return newFsverityContentSigner( + privateKey, "SHA256withRSA", PKCSObjectIdentifiers.rsaEncryption); + } + + private static ContentSigner newFsverityContentSigner( + PrivateKey privateKey, + String signatureAlgorithm, + ASN1ObjectIdentifier signatureAlgorithmIdOverride) + throws OperatorCreationException { + if (signatureAlgorithmIdOverride != null) { + return new ContentSignerWrapper( + new JcaContentSignerBuilder(signatureAlgorithm).build(privateKey)) { + @Override + public AlgorithmIdentifier getAlgorithmIdentifier() { + return new AlgorithmIdentifier(signatureAlgorithmIdOverride); + } + }; + } else { + return new JcaContentSignerBuilder(signatureAlgorithm).build(privateKey); + } + } + + private static X509CertificateHolder newX509CertificateHolder( + ContentSigner contentSigner, PublicKey publicKey, String name) { + // Time doesn't really matter, as we only care about the key. + Instant now = Instant.now(); + + return new X509v3CertificateBuilder( + new X500Name("CN=Issuer " + name), + /* serial= */ BigInteger.valueOf(now.getEpochSecond()), + new Date(now.minus(Duration.ofDays(1)).toEpochMilli()), + new Date(now.plus(Duration.ofDays(1)).toEpochMilli()), + new X500Name("CN=Subject " + name), + SubjectPublicKeyInfo.getInstance(publicKey.getEncoded())) + .build(contentSigner); + } +} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationSpec.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationSpec.java index ef5ea563de12..a7d47ef81687 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationSpec.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationSpec.java @@ -161,7 +161,7 @@ class TaskFragmentAnimationSpec { // The position should be 0-based as we will post translate in // TaskFragmentAnimationAdapter#onAnimationUpdate final Animation endTranslate = new TranslateAnimation(startBounds.left - endBounds.left, 0, - 0, 0); + startBounds.top - endBounds.top, 0); endTranslate.setDuration(CHANGE_ANIMATION_DURATION); endSet.addAnimation(endTranslate); // The end leash is resizing, we should update the window crop based on the clip rect. diff --git a/libs/WindowManager/Shell/res/values/config.xml b/libs/WindowManager/Shell/res/values/config.xml index 30c3d50ed8ad..df5f921f3a62 100644 --- a/libs/WindowManager/Shell/res/values/config.xml +++ b/libs/WindowManager/Shell/res/values/config.xml @@ -23,6 +23,10 @@ TODO(b/238217847): This config is temporary until we refactor the base WMComponent. --> <bool name="config_registerShellTaskOrganizerOnInit">true</bool> + <!-- Determines whether to register the shell transitions on init. + TODO(b/238217847): This config is temporary until we refactor the base WMComponent. --> + <bool name="config_registerShellTransitionsOnInit">true</bool> + <!-- Animation duration for PIP when entering. --> <integer name="config_pipEnterAnimationDuration">425</integer> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java index 48c5f64e0dd4..bd2ea9c1f822 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java @@ -41,7 +41,6 @@ import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; import com.android.wm.shell.common.SyncTransactionQueue; -import com.android.wm.shell.transition.Transitions; import java.io.PrintWriter; import java.util.concurrent.Executor; @@ -122,7 +121,7 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback, /** Until all users are converted, we may have mixed-use (eg. Car). */ private boolean isUsingShellTransitions() { - return mTaskViewTransitions != null && Transitions.ENABLE_SHELL_TRANSITIONS; + return mTaskViewTransitions != null && mTaskViewTransitions.isEnabled(); } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewTransitions.java index 83335ac24799..07d501201105 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewTransitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewTransitions.java @@ -87,6 +87,10 @@ public class TaskViewTransitions implements Transitions.TransitionHandler { // Note: Don't unregister handler since this is a singleton with lifetime bound to Shell } + boolean isEnabled() { + return mTransitions.isRegistered(); + } + /** * Looks through the pending transitions for one matching `taskView`. * @param taskView the pending transition should be for this. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java index 725b20525bf7..3972b592c448 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java @@ -671,10 +671,18 @@ public class BubbleController implements ConfigurationChangeListener { return; } + mAddedToWindowManager = false; + // Put on background for this binder call, was causing jank + mBackgroundExecutor.execute(() -> { + try { + mContext.unregisterReceiver(mBroadcastReceiver); + } catch (IllegalArgumentException e) { + // Not sure if this happens in production, but was happening in tests + // (b/253647225) + e.printStackTrace(); + } + }); try { - mAddedToWindowManager = false; - // Put on background for this binder call, was causing jank - mBackgroundExecutor.execute(() -> mContext.unregisterReceiver(mBroadcastReceiver)); if (mStackView != null) { mWindowManager.removeView(mStackView); mBubbleData.getOverflow().cleanUpExpandedState(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java index 66202ad1cbfd..bb7c4134aaaf 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java @@ -32,16 +32,16 @@ import android.view.IWindowManager; import android.view.InsetsSource; import android.view.InsetsSourceControl; import android.view.InsetsState; -import android.view.InsetsVisibilities; import android.view.Surface; import android.view.SurfaceControl; import android.view.WindowInsets; +import android.view.WindowInsets.Type.InsetsType; import android.view.animation.Interpolator; import android.view.animation.PathInterpolator; +import android.view.inputmethod.InputMethodManagerGlobal; import androidx.annotation.VisibleForTesting; -import com.android.internal.inputmethod.IInputMethodManagerGlobal; import com.android.wm.shell.sysui.ShellInit; import java.util.ArrayList; @@ -207,7 +207,7 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged public class PerDisplay implements DisplayInsetsController.OnInsetsChangedListener { final int mDisplayId; final InsetsState mInsetsState = new InsetsState(); - final InsetsVisibilities mRequestedVisibilities = new InsetsVisibilities(); + @InsetsType int mRequestedVisibleTypes = WindowInsets.Type.defaultVisible(); InsetsSourceControl mImeSourceControl = null; int mAnimationDirection = DIRECTION_NONE; ValueAnimator mAnimation = null; @@ -330,8 +330,7 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged } @Override - public void topFocusedWindowChanged(ComponentName component, - InsetsVisibilities requestedVisibilities) { + public void topFocusedWindowChanged(ComponentName component, int requestedVisibleTypes) { // Do nothing } @@ -340,10 +339,12 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged */ private void setVisibleDirectly(boolean visible) { mInsetsState.getSource(InsetsState.ITYPE_IME).setVisible(visible); - mRequestedVisibilities.setVisibility(InsetsState.ITYPE_IME, visible); + mRequestedVisibleTypes = visible + ? mRequestedVisibleTypes | WindowInsets.Type.ime() + : mRequestedVisibleTypes & ~WindowInsets.Type.ime(); try { - mWmService.updateDisplayWindowRequestedVisibilities(mDisplayId, - mRequestedVisibilities); + mWmService.updateDisplayWindowRequestedVisibleTypes(mDisplayId, + mRequestedVisibleTypes); } catch (RemoteException e) { } } @@ -514,7 +515,7 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged void removeImeSurface() { // Remove the IME surface to make the insets invisible for // non-client controlled insets. - IInputMethodManagerGlobal.removeImeSurface( + InputMethodManagerGlobal.removeImeSurface( e -> Slog.e(TAG, "Failed to remove IME surface.", e)); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java index 90a01f8c5295..8d4a09d8f371 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java @@ -24,7 +24,7 @@ import android.view.IDisplayWindowInsetsController; import android.view.IWindowManager; import android.view.InsetsSourceControl; import android.view.InsetsState; -import android.view.InsetsVisibilities; +import android.view.WindowInsets.Type.InsetsType; import androidx.annotation.BinderThread; @@ -177,13 +177,13 @@ public class DisplayInsetsController implements DisplayController.OnDisplaysChan } private void topFocusedWindowChanged(ComponentName component, - InsetsVisibilities requestedVisibilities) { + @InsetsType int requestedVisibleTypes) { CopyOnWriteArrayList<OnInsetsChangedListener> listeners = mListeners.get(mDisplayId); if (listeners == null) { return; } for (OnInsetsChangedListener listener : listeners) { - listener.topFocusedWindowChanged(component, requestedVisibilities); + listener.topFocusedWindowChanged(component, requestedVisibleTypes); } } @@ -192,9 +192,9 @@ public class DisplayInsetsController implements DisplayController.OnDisplaysChan extends IDisplayWindowInsetsController.Stub { @Override public void topFocusedWindowChanged(ComponentName component, - InsetsVisibilities requestedVisibilities) throws RemoteException { + @InsetsType int requestedVisibleTypes) throws RemoteException { mMainExecutor.execute(() -> { - PerDisplay.this.topFocusedWindowChanged(component, requestedVisibilities); + PerDisplay.this.topFocusedWindowChanged(component, requestedVisibleTypes); }); } @@ -239,11 +239,13 @@ public class DisplayInsetsController implements DisplayController.OnDisplaysChan /** * Called when top focused window changes to determine whether or not to take over insets * control. Won't be called if config_remoteInsetsControllerControlsSystemBars is false. + * * @param component The application component that is open in the top focussed window. - * @param requestedVisibilities The insets visibilities requested by the focussed window. + * @param requestedVisibleTypes The {@link InsetsType} requested visible by the focused + * window. */ default void topFocusedWindowChanged(ComponentName component, - InsetsVisibilities requestedVisibilities) {} + @InsetsType int requestedVisibleTypes) {} /** * Called when the window insets configuration has changed. @@ -259,17 +261,17 @@ public class DisplayInsetsController implements DisplayController.OnDisplaysChan /** * Called when a set of insets source window should be shown by policy. * - * @param types internal insets types (WindowInsets.Type.InsetsType) to show + * @param types {@link InsetsType} to show * @param fromIme true if this request originated from IME (InputMethodService). */ - default void showInsets(int types, boolean fromIme) {} + default void showInsets(@InsetsType int types, boolean fromIme) {} /** * Called when a set of insets source window should be hidden by policy. * - * @param types internal insets types (WindowInsets.Type.InsetsType) to hide + * @param types {@link InsetsType} to hide * @param fromIme true if this request originated from IME (InputMethodService). */ - default void hideInsets(int types, boolean fromIme) {} + default void hideInsets(@InsetsType int types, boolean fromIme) {} } -}
\ No newline at end of file +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java index 64dbfbbb738d..8b8e192cf964 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java @@ -507,6 +507,10 @@ public abstract class WMShellBaseModule { @ShellMainThread ShellExecutor mainExecutor, @ShellMainThread Handler mainHandler, @ShellAnimationThread ShellExecutor animExecutor) { + if (!context.getResources().getBoolean(R.bool.config_registerShellTransitionsOnInit)) { + // TODO(b/238217847): Force override shell init if registration is disabled + shellInit = new ShellInit(mainExecutor); + } return new Transitions(context, shellInit, shellController, organizer, pool, displayController, mainExecutor, mainHandler, animExecutor); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java index 195ff502e7dc..2fafe67664f8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java @@ -48,7 +48,6 @@ public class DesktopModeStatus { try { int result = Settings.System.getIntForUser(context.getContentResolver(), Settings.System.DESKTOP_MODE, UserHandle.USER_CURRENT); - ProtoLog.d(WM_SHELL_DESKTOP_MODE, "isDesktopModeEnabled=%s", result); return result != 0; } catch (Exception e) { ProtoLog.e(WM_SHELL_DESKTOP_MODE, "Failed to read DESKTOP_MODE setting %s", e); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt index c91d54a62ae6..b7749fc4c3d4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt @@ -69,22 +69,28 @@ class DesktopModeTaskRepository { /** * Mark a task with given [taskId] as active. + * + * @return `true` if the task was not active */ - fun addActiveTask(taskId: Int) { + fun addActiveTask(taskId: Int): Boolean { val added = activeTasks.add(taskId) if (added) { activeTasksListeners.onEach { it.onActiveTasksChanged() } } + return added } /** * Remove task with given [taskId] from active tasks. + * + * @return `true` if the task was active */ - fun removeActiveTask(taskId: Int) { + fun removeActiveTask(taskId: Int): Boolean { val removed = activeTasks.remove(taskId) if (removed) { activeTasksListeners.onEach { it.onActiveTasksChanged() } } + return removed } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java index eaa7158abbe5..90b35a5a55e1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java @@ -87,11 +87,13 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener { } if (DesktopModeStatus.IS_SUPPORTED && taskInfo.isVisible) { - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, - "Adding active freeform task: #%d", taskInfo.taskId); - mDesktopModeTaskRepository.ifPresent(it -> it.addActiveTask(taskInfo.taskId)); - mDesktopModeTaskRepository.ifPresent( - it -> it.updateVisibleFreeformTasks(taskInfo.taskId, true)); + mDesktopModeTaskRepository.ifPresent(repository -> { + if (repository.addActiveTask(taskInfo.taskId)) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, + "Adding active freeform task: #%d", taskInfo.taskId); + } + repository.updateVisibleFreeformTasks(taskInfo.taskId, true); + }); } } @@ -102,11 +104,13 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener { mTasks.remove(taskInfo.taskId); if (DesktopModeStatus.IS_SUPPORTED) { - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, - "Removing active freeform task: #%d", taskInfo.taskId); - mDesktopModeTaskRepository.ifPresent(it -> it.removeActiveTask(taskInfo.taskId)); - mDesktopModeTaskRepository.ifPresent( - it -> it.updateVisibleFreeformTasks(taskInfo.taskId, false)); + mDesktopModeTaskRepository.ifPresent(repository -> { + if (repository.removeActiveTask(taskInfo.taskId)) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, + "Removing active freeform task: #%d", taskInfo.taskId); + } + repository.updateVisibleFreeformTasks(taskInfo.taskId, false); + }); } if (!Transitions.ENABLE_SHELL_TRANSITIONS) { @@ -123,13 +127,15 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener { mWindowDecorationViewModel.onTaskInfoChanged(state.mTaskInfo); if (DesktopModeStatus.IS_SUPPORTED) { - if (taskInfo.isVisible) { - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, - "Adding active freeform task: #%d", taskInfo.taskId); - mDesktopModeTaskRepository.ifPresent(it -> it.addActiveTask(taskInfo.taskId)); - } - mDesktopModeTaskRepository.ifPresent( - it -> it.updateVisibleFreeformTasks(taskInfo.taskId, taskInfo.isVisible)); + mDesktopModeTaskRepository.ifPresent(repository -> { + if (taskInfo.isVisible) { + if (repository.addActiveTask(taskInfo.taskId)) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, + "Adding active freeform task: #%d", taskInfo.taskId); + } + } + repository.updateVisibleFreeformTasks(taskInfo.taskId, taskInfo.isVisible); + }); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java index b9746e338ced..cbed4b5a501f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java @@ -115,8 +115,8 @@ public class PipSurfaceTransactionHelper { // coordinates so offset the bounds to 0,0 mTmpDestinationRect.offsetTo(0, 0); mTmpDestinationRect.inset(insets); - // Scale by the shortest edge and offset such that the top/left of the scaled inset source - // rect aligns with the top/left of the destination bounds + // Scale to the bounds no smaller than the destination and offset such that the top/left + // of the scaled inset source rect aligns with the top/left of the destination bounds final float scale; if (isInPipDirection && sourceRectHint != null && sourceRectHint.width() < sourceBounds.width()) { @@ -129,9 +129,8 @@ public class PipSurfaceTransactionHelper { : (float) destinationBounds.height() / sourceBounds.height(); scale = (1 - fraction) * startScale + fraction * endScale; } else { - scale = sourceBounds.width() <= sourceBounds.height() - ? (float) destinationBounds.width() / sourceBounds.width() - : (float) destinationBounds.height() / sourceBounds.height(); + scale = Math.max((float) destinationBounds.width() / sourceBounds.width(), + (float) destinationBounds.height() / sourceBounds.height()); } final float left = destinationBounds.left - insets.left * scale; final float top = destinationBounds.top - insets.top * scale; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java index d7ca791e3863..21a13103616c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java @@ -108,6 +108,14 @@ class SplitScreenTransitions { private void playInternalAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t, @NonNull WindowContainerToken mainRoot, @NonNull WindowContainerToken sideRoot, @NonNull WindowContainerToken topRoot) { + final TransitSession pendingTransition = getPendingTransition(transition); + if (pendingTransition != null && pendingTransition.mCanceled) { + // The pending transition was canceled, so skip playing animation. + t.apply(); + onFinish(null /* wct */, null /* wctCB */); + return; + } + // Play some place-holder fade animations for (int i = info.getChanges().size() - 1; i >= 0; --i) { final TransitionInfo.Change change = info.getChanges().get(i); @@ -170,9 +178,7 @@ class SplitScreenTransitions { } boolean isPendingTransition(IBinder transition) { - return isPendingEnter(transition) - || isPendingDismiss(transition) - || isPendingRecent(transition); + return getPendingTransition(transition) != null; } boolean isPendingEnter(IBinder transition) { @@ -187,22 +193,38 @@ class SplitScreenTransitions { return mPendingDismiss != null && mPendingDismiss.mTransition == transition; } + @Nullable + private TransitSession getPendingTransition(IBinder transition) { + if (isPendingEnter(transition)) { + return mPendingEnter; + } else if (isPendingRecent(transition)) { + return mPendingRecent; + } else if (isPendingDismiss(transition)) { + return mPendingDismiss; + } + + return null; + } + /** Starts a transition to enter split with a remote transition animator. */ IBinder startEnterTransition( @WindowManager.TransitionType int transitType, WindowContainerTransaction wct, @Nullable RemoteTransition remoteTransition, Transitions.TransitionHandler handler, - @Nullable TransitionCallback callback) { + @Nullable TransitionConsumedCallback consumedCallback, + @Nullable TransitionFinishedCallback finishedCallback) { final IBinder transition = mTransitions.startTransition(transitType, wct, handler); - setEnterTransition(transition, remoteTransition, callback); + setEnterTransition(transition, remoteTransition, consumedCallback, finishedCallback); return transition; } /** Sets a transition to enter split. */ void setEnterTransition(@NonNull IBinder transition, - @Nullable RemoteTransition remoteTransition, @Nullable TransitionCallback callback) { - mPendingEnter = new TransitSession(transition, callback); + @Nullable RemoteTransition remoteTransition, + @Nullable TransitionConsumedCallback consumedCallback, + @Nullable TransitionFinishedCallback finishedCallback) { + mPendingEnter = new TransitSession(transition, consumedCallback, finishedCallback); if (remoteTransition != null) { // Wrapping it for ease-of-use (OneShot handles all the binder linking/death stuff) @@ -237,8 +259,9 @@ class SplitScreenTransitions { } void setRecentTransition(@NonNull IBinder transition, - @Nullable RemoteTransition remoteTransition, @Nullable TransitionCallback callback) { - mPendingRecent = new TransitSession(transition, callback); + @Nullable RemoteTransition remoteTransition, + @Nullable TransitionFinishedCallback finishCallback) { + mPendingRecent = new TransitSession(transition, null /* consumedCb */, finishCallback); if (remoteTransition != null) { // Wrapping it for ease-of-use (OneShot handles all the binder linking/death stuff) @@ -248,7 +271,7 @@ class SplitScreenTransitions { } ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " splitTransition " - + " deduced Enter recent panel"); + + " deduced Enter recent panel"); } void mergeAnimation(IBinder transition, TransitionInfo info, SurfaceControl.Transaction t, @@ -256,14 +279,9 @@ class SplitScreenTransitions { if (mergeTarget != mAnimatingTransition) return; if (isPendingEnter(transition) && isPendingRecent(mergeTarget)) { - mPendingRecent.mCallback = new TransitionCallback() { - @Override - public void onTransitionFinished(WindowContainerTransaction finishWct, - SurfaceControl.Transaction finishT) { - // Since there's an entering transition merged, recent transition no longer - // need to handle entering split screen after the transition finished. - } - }; + // Since there's an entering transition merged, recent transition no longer + // need to handle entering split screen after the transition finished. + mPendingRecent.setFinishedCallback(null); } if (mActiveRemoteHandler != null) { @@ -277,7 +295,7 @@ class SplitScreenTransitions { } boolean end() { - // If its remote, there's nothing we can do right now. + // If It's remote, there's nothing we can do right now. if (mActiveRemoteHandler != null) return false; for (int i = mAnimations.size() - 1; i >= 0; --i) { final Animator anim = mAnimations.get(i); @@ -290,20 +308,20 @@ class SplitScreenTransitions { @Nullable SurfaceControl.Transaction finishT) { if (isPendingEnter(transition)) { if (!aborted) { - // An enter transition got merged, appends the rest operations to finish entering + // An entering transition got merged, appends the rest operations to finish entering // split screen. mStageCoordinator.finishEnterSplitScreen(finishT); mPendingRemoteHandler = null; } - mPendingEnter.mCallback.onTransitionConsumed(aborted); + mPendingEnter.onConsumed(aborted); mPendingEnter = null; mPendingRemoteHandler = null; } else if (isPendingDismiss(transition)) { - mPendingDismiss.mCallback.onTransitionConsumed(aborted); + mPendingDismiss.onConsumed(aborted); mPendingDismiss = null; } else if (isPendingRecent(transition)) { - mPendingRecent.mCallback.onTransitionConsumed(aborted); + mPendingRecent.onConsumed(aborted); mPendingRecent = null; mPendingRemoteHandler = null; } @@ -312,23 +330,16 @@ class SplitScreenTransitions { void onFinish(WindowContainerTransaction wct, WindowContainerTransactionCallback wctCB) { if (!mAnimations.isEmpty()) return; - TransitionCallback callback = null; + if (wct == null) wct = new WindowContainerTransaction(); if (isPendingEnter(mAnimatingTransition)) { - callback = mPendingEnter.mCallback; + mPendingEnter.onFinished(wct, mFinishTransaction); mPendingEnter = null; - } - if (isPendingDismiss(mAnimatingTransition)) { - callback = mPendingDismiss.mCallback; - mPendingDismiss = null; - } - if (isPendingRecent(mAnimatingTransition)) { - callback = mPendingRecent.mCallback; + } else if (isPendingRecent(mAnimatingTransition)) { + mPendingRecent.onFinished(wct, mFinishTransaction); mPendingRecent = null; - } - - if (callback != null) { - if (wct == null) wct = new WindowContainerTransaction(); - callback.onTransitionFinished(wct, mFinishTransaction); + } else if (isPendingDismiss(mAnimatingTransition)) { + mPendingDismiss.onFinished(wct, mFinishTransaction); + mPendingDismiss = null; } mPendingRemoteHandler = null; @@ -363,10 +374,7 @@ class SplitScreenTransitions { onFinish(null /* wct */, null /* wctCB */); }); }; - va.addListener(new Animator.AnimatorListener() { - @Override - public void onAnimationStart(Animator animation) { } - + va.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { finisher.run(); @@ -376,9 +384,6 @@ class SplitScreenTransitions { public void onAnimationCancel(Animator animation) { finisher.run(); } - - @Override - public void onAnimationRepeat(Animator animation) { } }); mAnimations.add(va); mTransitions.getAnimExecutor().execute(va::start); @@ -432,24 +437,66 @@ class SplitScreenTransitions { || info.getType() == TRANSIT_SPLIT_SCREEN_PAIR_OPEN; } - /** Clean-up callbacks for transition. */ - interface TransitionCallback { - /** Calls when the transition got consumed. */ - default void onTransitionConsumed(boolean aborted) {} + /** Calls when the transition got consumed. */ + interface TransitionConsumedCallback { + void onConsumed(boolean aborted); + } - /** Calls when the transition finished. */ - default void onTransitionFinished(WindowContainerTransaction finishWct, - SurfaceControl.Transaction finishT) {} + /** Calls when the transition finished. */ + interface TransitionFinishedCallback { + void onFinished(WindowContainerTransaction wct, SurfaceControl.Transaction t); } /** Session for a transition and its clean-up callback. */ static class TransitSession { final IBinder mTransition; - TransitionCallback mCallback; + TransitionConsumedCallback mConsumedCallback; + TransitionFinishedCallback mFinishedCallback; - TransitSession(IBinder transition, @Nullable TransitionCallback callback) { + /** Whether the transition was canceled. */ + boolean mCanceled; + + TransitSession(IBinder transition, + @Nullable TransitionConsumedCallback consumedCallback, + @Nullable TransitionFinishedCallback finishedCallback) { mTransition = transition; - mCallback = callback != null ? callback : new TransitionCallback() {}; + mConsumedCallback = consumedCallback; + mFinishedCallback = finishedCallback; + + } + + /** Sets transition consumed callback. */ + void setConsumedCallback(@Nullable TransitionConsumedCallback callback) { + mConsumedCallback = callback; + } + + /** Sets transition finished callback. */ + void setFinishedCallback(@Nullable TransitionFinishedCallback callback) { + mFinishedCallback = callback; + } + + /** + * Cancels the transition. This should be called before playing animation. A canceled + * transition will skip playing animation. + * + * @param finishedCb new finish callback to override. + */ + void cancel(@Nullable TransitionFinishedCallback finishedCb) { + mCanceled = true; + setFinishedCallback(finishedCb); + } + + void onConsumed(boolean aborted) { + if (mConsumedCallback != null) { + mConsumedCallback.onConsumed(aborted); + } + } + + void onFinished(WindowContainerTransaction finishWct, + SurfaceControl.Transaction finishT) { + if (mFinishedCallback != null) { + mFinishedCallback.onFinished(finishWct, finishT); + } } } @@ -459,7 +506,7 @@ class SplitScreenTransitions { final @SplitScreen.StageType int mDismissTop; DismissTransition(IBinder transition, int reason, int dismissTop) { - super(transition, null /* callback */); + super(transition, null /* consumedCallback */, null /* finishedCallback */); this.mReason = reason; this.mDismissTop = dismissTop; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index e2ac01f7b003..943419bb8ea2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -226,33 +226,36 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } }; - private final SplitScreenTransitions.TransitionCallback mRecentTransitionCallback = - new SplitScreenTransitions.TransitionCallback() { - @Override - public void onTransitionFinished(WindowContainerTransaction finishWct, - SurfaceControl.Transaction finishT) { - // Check if the recent transition is finished by returning to the current split, so we - // can restore the divider bar. - for (int i = 0; i < finishWct.getHierarchyOps().size(); ++i) { - final WindowContainerTransaction.HierarchyOp op = - finishWct.getHierarchyOps().get(i); - final IBinder container = op.getContainer(); - if (op.getType() == HIERARCHY_OP_TYPE_REORDER && op.getToTop() - && (mMainStage.containsContainer(container) - || mSideStage.containsContainer(container))) { - updateSurfaceBounds(mSplitLayout, finishT, false /* applyResizingOffset */); - setDividerVisibility(true, finishT); - return; - } - } + private final SplitScreenTransitions.TransitionFinishedCallback + mRecentTransitionFinishedCallback = + new SplitScreenTransitions.TransitionFinishedCallback() { + @Override + public void onFinished(WindowContainerTransaction finishWct, + SurfaceControl.Transaction finishT) { + // Check if the recent transition is finished by returning to the current + // split, so we + // can restore the divider bar. + for (int i = 0; i < finishWct.getHierarchyOps().size(); ++i) { + final WindowContainerTransaction.HierarchyOp op = + finishWct.getHierarchyOps().get(i); + final IBinder container = op.getContainer(); + if (op.getType() == HIERARCHY_OP_TYPE_REORDER && op.getToTop() + && (mMainStage.containsContainer(container) + || mSideStage.containsContainer(container))) { + updateSurfaceBounds(mSplitLayout, finishT, + false /* applyResizingOffset */); + setDividerVisibility(true, finishT); + return; + } + } - // Dismiss the split screen if it's not returning to split. - prepareExitSplitScreen(STAGE_TYPE_UNDEFINED, finishWct); - setSplitsVisible(false); - setDividerVisibility(false, finishT); - logExit(EXIT_REASON_UNKNOWN); - } - }; + // Dismiss the split screen if it's not returning to split. + prepareExitSplitScreen(STAGE_TYPE_UNDEFINED, finishWct); + setSplitsVisible(false); + setDividerVisibility(false, finishT); + logExit(EXIT_REASON_UNKNOWN); + } + }; protected StageCoordinator(Context context, int displayId, SyncTransactionQueue syncQueue, ShellTaskOrganizer taskOrganizer, DisplayController displayController, @@ -389,15 +392,11 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, if (ENABLE_SHELL_TRANSITIONS) { prepareEnterSplitScreen(wct); mSplitTransitions.startEnterTransition(TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE, wct, - null, this, new SplitScreenTransitions.TransitionCallback() { - @Override - public void onTransitionFinished(WindowContainerTransaction finishWct, - SurfaceControl.Transaction finishT) { - if (!evictWct.isEmpty()) { - finishWct.merge(evictWct, true); - } + null, this, null /* consumedCallback */, (finishWct, finishT) -> { + if (!evictWct.isEmpty()) { + finishWct.merge(evictWct, true); } - }); + } /* finishedCallback */); } else { if (!evictWct.isEmpty()) { wct.merge(evictWct, true /* transfer */); @@ -434,28 +433,25 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, options = resolveStartStage(STAGE_TYPE_UNDEFINED, position, options, null /* wct */); wct.sendPendingIntent(intent, fillInIntent, options); + + // If split screen is not activated, we're expecting to open a pair of apps to split. + final int transitType = mMainStage.isActive() + ? TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE : TRANSIT_SPLIT_SCREEN_PAIR_OPEN; prepareEnterSplitScreen(wct, null /* taskInfo */, position); - mSplitTransitions.startEnterTransition(TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE, wct, null, this, - new SplitScreenTransitions.TransitionCallback() { - @Override - public void onTransitionConsumed(boolean aborted) { - // Switch the split position if launching as MULTIPLE_TASK failed. - if (aborted - && (fillInIntent.getFlags() & FLAG_ACTIVITY_MULTIPLE_TASK) != 0) { - setSideStagePositionAnimated( - SplitLayout.reversePosition(mSideStagePosition)); - } + mSplitTransitions.startEnterTransition(transitType, wct, null, this, + aborted -> { + // Switch the split position if launching as MULTIPLE_TASK failed. + if (aborted && (fillInIntent.getFlags() & FLAG_ACTIVITY_MULTIPLE_TASK) != 0) { + setSideStagePositionAnimated( + SplitLayout.reversePosition(mSideStagePosition)); } - - @Override - public void onTransitionFinished(WindowContainerTransaction finishWct, - SurfaceControl.Transaction finishT) { - if (!evictWct.isEmpty()) { - finishWct.merge(evictWct, true); - } + } /* consumedCallback */, + (finishWct, finishT) -> { + if (!evictWct.isEmpty()) { + finishWct.merge(evictWct, true); } - }); + } /* finishedCallback */); } /** Launches an activity into split by legacy transition. */ @@ -564,9 +560,9 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, /** * Starts with the second task to a split pair in one transition. * - * @param wct transaction to start the first task + * @param wct transaction to start the first task * @param instanceId if {@code null}, will not log. Otherwise it will be used in - * {@link SplitscreenEventLogger#logEnter(float, int, int, int, int, boolean)} + * {@link SplitscreenEventLogger#logEnter(float, int, int, int, int, boolean)} */ private void startWithTask(WindowContainerTransaction wct, int mainTaskId, @Nullable Bundle mainOptions, float splitRatio, @@ -592,7 +588,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, wct.startTask(mainTaskId, mainOptions); mSplitTransitions.startEnterTransition( - TRANSIT_SPLIT_SCREEN_PAIR_OPEN, wct, remoteTransition, this, null); + TRANSIT_SPLIT_SCREEN_PAIR_OPEN, wct, remoteTransition, this, null, null); setEnterInstanceId(instanceId); } @@ -639,7 +635,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } /** - * @param wct transaction to start the first task + * @param wct transaction to start the first task * @param instanceId if {@code null}, will not log. Otherwise it will be used in * {@link SplitscreenEventLogger#logEnter(float, int, int, int, int, boolean)} */ @@ -1082,15 +1078,15 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, switch (exitReason) { // One of the apps doesn't support MW case EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW: - // User has explicitly dragged the divider to dismiss split + // User has explicitly dragged the divider to dismiss split case EXIT_REASON_DRAG_DIVIDER: - // Either of the split apps have finished + // Either of the split apps have finished case EXIT_REASON_APP_FINISHED: - // One of the children enters PiP + // One of the children enters PiP case EXIT_REASON_CHILD_TASK_ENTER_PIP: - // One of the apps occludes lock screen. + // One of the apps occludes lock screen. case EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP: - // User has unlocked the device after folded + // User has unlocked the device after folded case EXIT_REASON_DEVICE_FOLDED: return true; default: @@ -1839,7 +1835,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, || activityType == ACTIVITY_TYPE_RECENTS) { // Enter overview panel, so start recent transition. mSplitTransitions.setRecentTransition(transition, request.getRemoteTransition(), - mRecentTransitionCallback); + mRecentTransitionFinishedCallback); } else if (mSplitTransitions.mPendingRecent == null) { // If split-task is not controlled by recents animation // and occluded by the other fullscreen task, dismiss both. @@ -1853,8 +1849,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, // One task is appearing into split, prepare to enter split screen. out = new WindowContainerTransaction(); prepareEnterSplitScreen(out); - mSplitTransitions.setEnterTransition( - transition, request.getRemoteTransition(), null /* callback */); + mSplitTransitions.setEnterTransition(transition, request.getRemoteTransition(), + null /* consumedCallback */, null /* finishedCallback */); } } return out; @@ -1873,7 +1869,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } final @WindowManager.TransitionType int type = request.getType(); if (isSplitActive() && !isOpeningType(type) - && (mMainStage.getChildCount() == 0 || mSideStage.getChildCount() == 0)) { + && (mMainStage.getChildCount() == 0 || mSideStage.getChildCount() == 0)) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " One of the splits became " + "empty during a mixed transition (one not handled by split)," + " so make sure split-screen state is cleaned-up. " @@ -2031,17 +2027,21 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } } - // TODO(b/250853925): fallback logic. Probably start a new transition to exit split before - // applying anything here. Ideally consolidate with transition-merging. if (info.getType() == TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE) { if (mainChild == null && sideChild == null) { - throw new IllegalStateException("Launched a task in split, but didn't receive any" - + " task in transition."); + Log.w(TAG, "Launched a task in split, but didn't receive any task in transition."); + mSplitTransitions.mPendingEnter.cancel(null /* finishedCb */); + return true; } } else { if (mainChild == null || sideChild == null) { - throw new IllegalStateException("Launched 2 tasks in split, but didn't receive" + Log.w(TAG, "Launched 2 tasks in split, but didn't receive" + " 2 tasks in transition. Possibly one of them failed to launch"); + final int dismissTop = mainChild != null ? STAGE_TYPE_MAIN : + (sideChild != null ? STAGE_TYPE_SIDE : STAGE_TYPE_UNDEFINED); + mSplitTransitions.mPendingEnter.cancel( + (cancelWct, cancelT) -> prepareExitSplitScreen(dismissTop, cancelWct)); + return true; } } @@ -2305,7 +2305,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, final int stageType = isMainStage ? STAGE_TYPE_MAIN : STAGE_TYPE_SIDE; final WindowContainerTransaction wct = new WindowContainerTransaction(); prepareExitSplitScreen(stageType, wct); - mSplitTransitions.startDismissTransition(wct,StageCoordinator.this, stageType, + mSplitTransitions.startDismissTransition(wct, StageCoordinator.this, stageType, EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW); mSplitUnsupportedToast.show(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java index 7b498e4f54ec..3929e835cdaa 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java @@ -77,6 +77,7 @@ import android.view.SurfaceSession; import android.view.View; import android.view.ViewGroup; import android.view.WindowInsets; +import android.view.WindowInsets.Type.InsetsType; import android.view.WindowManager; import android.view.WindowManagerGlobal; import android.window.ClientWindowFrames; @@ -223,7 +224,7 @@ public class TaskSnapshotWindow { final TaskSnapshotWindow snapshotSurface = new TaskSnapshotWindow( surfaceControl, snapshot, layoutParams.getTitle(), taskDescription, appearance, windowFlags, windowPrivateFlags, taskBounds, orientation, activityType, - topWindowInsetsState, clearWindowHandler, splashScreenExecutor); + info.requestedVisibleTypes, clearWindowHandler, splashScreenExecutor); final Window window = snapshotSurface.mWindow; final InsetsState tmpInsetsState = new InsetsState(); @@ -233,7 +234,7 @@ public class TaskSnapshotWindow { try { Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "TaskSnapshot#addToDisplay"); final int res = session.addToDisplay(window, layoutParams, View.GONE, displayId, - info.requestedVisibilities, tmpInputChannel, tmpInsetsState, tmpControls, + info.requestedVisibleTypes, tmpInputChannel, tmpInsetsState, tmpControls, new Rect(), sizeCompatScale); Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); if (res < 0) { @@ -263,7 +264,7 @@ public class TaskSnapshotWindow { public TaskSnapshotWindow(SurfaceControl surfaceControl, TaskSnapshot snapshot, CharSequence title, TaskDescription taskDescription, int appearance, int windowFlags, int windowPrivateFlags, Rect taskBounds, - int currentOrientation, int activityType, InsetsState topWindowInsetsState, + int currentOrientation, int activityType, @InsetsType int requestedVisibleTypes, Runnable clearWindowHandler, ShellExecutor splashScreenExecutor) { mSplashScreenExecutor = splashScreenExecutor; mSession = WindowManagerGlobal.getWindowSession(); @@ -276,7 +277,7 @@ public class TaskSnapshotWindow { mBackgroundPaint.setColor(backgroundColor != 0 ? backgroundColor : WHITE); mTaskBounds = taskBounds; mSystemBarBackgroundPainter = new SystemBarBackgroundPainter(windowFlags, - windowPrivateFlags, appearance, taskDescription, 1f, topWindowInsetsState); + windowPrivateFlags, appearance, taskDescription, 1f, requestedVisibleTypes); mStatusBarColor = taskDescription.getStatusBarColor(); mOrientationOnCreation = currentOrientation; mActivityType = activityType; @@ -569,11 +570,12 @@ public class TaskSnapshotWindow { private final int mWindowFlags; private final int mWindowPrivateFlags; private final float mScale; - private final InsetsState mInsetsState; + private final @InsetsType int mRequestedVisibleTypes; private final Rect mSystemBarInsets = new Rect(); SystemBarBackgroundPainter(int windowFlags, int windowPrivateFlags, int appearance, - TaskDescription taskDescription, float scale, InsetsState insetsState) { + TaskDescription taskDescription, float scale, + @InsetsType int requestedVisibleTypes) { mWindowFlags = windowFlags; mWindowPrivateFlags = windowPrivateFlags; mScale = scale; @@ -592,7 +594,7 @@ public class TaskSnapshotWindow { && context.getResources().getBoolean(R.bool.config_navBarNeedsScrim)); mStatusBarPaint.setColor(mStatusBarColor); mNavigationBarPaint.setColor(mNavigationBarColor); - mInsetsState = insetsState; + mRequestedVisibleTypes = requestedVisibleTypes; } void setInsets(Rect systemBarInsets) { @@ -603,7 +605,7 @@ public class TaskSnapshotWindow { final boolean forceBarBackground = (mWindowPrivateFlags & PRIVATE_FLAG_FORCE_DRAW_BAR_BACKGROUNDS) != 0; if (STATUS_BAR_COLOR_VIEW_ATTRIBUTES.isVisible( - mInsetsState, mStatusBarColor, mWindowFlags, forceBarBackground)) { + mRequestedVisibleTypes, mStatusBarColor, mWindowFlags, forceBarBackground)) { return (int) (mSystemBarInsets.top * mScale); } else { return 0; @@ -614,7 +616,7 @@ public class TaskSnapshotWindow { final boolean forceBarBackground = (mWindowPrivateFlags & PRIVATE_FLAG_FORCE_DRAW_BAR_BACKGROUNDS) != 0; return NAVIGATION_BAR_COLOR_VIEW_ATTRIBUTES.isVisible( - mInsetsState, mNavigationBarColor, mWindowFlags, forceBarBackground); + mRequestedVisibleTypes, mNavigationBarColor, mWindowFlags, forceBarBackground); } void drawDecors(Canvas c, @Nullable Rect alreadyDrawnFrame) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java index 394d6f6bf731..63d31cde4715 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java @@ -122,6 +122,8 @@ public class Transitions implements RemoteCallable<Transitions> { private final ShellController mShellController; private final ShellTransitionImpl mImpl = new ShellTransitionImpl(); + private boolean mIsRegistered = false; + /** List of possible handlers. Ordered by specificity (eg. tapped back to front). */ private final ArrayList<TransitionHandler> mHandlers = new ArrayList<>(); @@ -163,19 +165,18 @@ public class Transitions implements RemoteCallable<Transitions> { displayController, pool, mainExecutor, mainHandler, animExecutor); mRemoteTransitionHandler = new RemoteTransitionHandler(mMainExecutor); mShellController = shellController; - shellInit.addInitCallback(this::onInit, this); - } - - private void onInit() { - mShellController.addExternalInterface(KEY_EXTRA_SHELL_SHELL_TRANSITIONS, - this::createExternalInterface, this); - // The very last handler (0 in the list) should be the default one. mHandlers.add(mDefaultTransitionHandler); ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "addHandler: Default"); // Next lowest priority is remote transitions. mHandlers.add(mRemoteTransitionHandler); ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "addHandler: Remote"); + shellInit.addInitCallback(this::onInit, this); + } + + private void onInit() { + mShellController.addExternalInterface(KEY_EXTRA_SHELL_SHELL_TRANSITIONS, + this::createExternalInterface, this); ContentResolver resolver = mContext.getContentResolver(); mTransitionAnimationScaleSetting = getTransitionAnimationScaleSetting(); @@ -186,13 +187,23 @@ public class Transitions implements RemoteCallable<Transitions> { new SettingsObserver()); if (Transitions.ENABLE_SHELL_TRANSITIONS) { + mIsRegistered = true; // Register this transition handler with Core - mOrganizer.registerTransitionPlayer(mPlayerImpl); + try { + mOrganizer.registerTransitionPlayer(mPlayerImpl); + } catch (RuntimeException e) { + mIsRegistered = false; + throw e; + } // Pre-load the instance. TransitionMetrics.getInstance(); } } + public boolean isRegistered() { + return mIsRegistered; + } + private float getTransitionAnimationScaleSetting() { return fixScale(Settings.Global.getFloat(mContext.getContentResolver(), Settings.Global.TRANSITION_ANIMATION_SCALE, mContext.getResources().getFloat( diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayInsetsControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayInsetsControllerTest.java index 5f5a3c584ee0..39db328ef0e0 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayInsetsControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayInsetsControllerTest.java @@ -32,7 +32,7 @@ import android.view.IDisplayWindowInsetsController; import android.view.IWindowManager; import android.view.InsetsSourceControl; import android.view.InsetsState; -import android.view.InsetsVisibilities; +import android.view.WindowInsets; import androidx.test.filters.SmallTest; @@ -108,7 +108,7 @@ public class DisplayInsetsControllerTest extends ShellTestCase { mController.addInsetsChangedListener(SECOND_DISPLAY, secondListener); mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).topFocusedWindowChanged(null, - new InsetsVisibilities()); + WindowInsets.Type.defaultVisible()); mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).insetsChanged(null); mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).insetsControlChanged(null, null); mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).showInsets(0, false); @@ -128,7 +128,7 @@ public class DisplayInsetsControllerTest extends ShellTestCase { assertTrue(secondListener.hideInsetsCount == 0); mInsetsControllersByDisplayId.get(SECOND_DISPLAY).topFocusedWindowChanged(null, - new InsetsVisibilities()); + WindowInsets.Type.defaultVisible()); mInsetsControllersByDisplayId.get(SECOND_DISPLAY).insetsChanged(null); mInsetsControllersByDisplayId.get(SECOND_DISPLAY).insetsControlChanged(null, null); mInsetsControllersByDisplayId.get(SECOND_DISPLAY).showInsets(0, false); @@ -175,8 +175,7 @@ public class DisplayInsetsControllerTest extends ShellTestCase { int hideInsetsCount = 0; @Override - public void topFocusedWindowChanged(ComponentName component, - InsetsVisibilities requestedVisibilities) { + public void topFocusedWindowChanged(ComponentName component, int requestedVisibleTypes) { topFocusedWindowChangedCount++; } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java index ea0033ba4bbb..652f9b38c88f 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java @@ -181,7 +181,7 @@ public class SplitTransitionTests extends ShellTestCase { IBinder transition = mSplitScreenTransitions.startEnterTransition( TRANSIT_SPLIT_SCREEN_PAIR_OPEN, new WindowContainerTransaction(), - new RemoteTransition(testRemote), mStageCoordinator, null); + new RemoteTransition(testRemote), mStageCoordinator, null, null); mMainStage.onTaskAppeared(mMainChild, createMockSurface()); mSideStage.onTaskAppeared(mSideChild, createMockSurface()); boolean accepted = mStageCoordinator.startAnimation(transition, info, @@ -421,7 +421,7 @@ public class SplitTransitionTests extends ShellTestCase { TransitionInfo enterInfo = createEnterPairInfo(); IBinder enterTransit = mSplitScreenTransitions.startEnterTransition( TRANSIT_SPLIT_SCREEN_PAIR_OPEN, new WindowContainerTransaction(), - new RemoteTransition(new TestRemoteTransition()), mStageCoordinator, null); + new RemoteTransition(new TestRemoteTransition()), mStageCoordinator, null, null); mMainStage.onTaskAppeared(mMainChild, createMockSurface()); mSideStage.onTaskAppeared(mSideChild, createMockSurface()); mStageCoordinator.startAnimation(enterTransit, enterInfo, diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java index e5ae2962e6e4..11fda8bf7bbc 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java @@ -249,7 +249,7 @@ public class StartingSurfaceDrawerTests extends ShellTestCase { doReturn(WindowManagerGlobal.ADD_OKAY).when(session).addToDisplay( any() /* window */, any() /* attrs */, anyInt() /* viewVisibility */, anyInt() /* displayId */, - any() /* requestedVisibility */, any() /* outInputChannel */, + anyInt() /* requestedVisibleTypes */, any() /* outInputChannel */, any() /* outInsetsState */, any() /* outActiveControls */, any() /* outAttachedFrame */, any() /* outSizeCompatScale */); TaskSnapshotWindow mockSnapshotWindow = TaskSnapshotWindow.create(windowInfo, diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/TaskSnapshotWindowTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/TaskSnapshotWindowTest.java index 3de50bb60470..004df2a29e58 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/TaskSnapshotWindowTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/TaskSnapshotWindowTest.java @@ -39,9 +39,9 @@ import android.graphics.ColorSpace; import android.graphics.Point; import android.graphics.Rect; import android.hardware.HardwareBuffer; -import android.view.InsetsState; import android.view.Surface; import android.view.SurfaceControl; +import android.view.WindowInsets; import android.window.TaskSnapshot; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -84,7 +84,8 @@ public class TaskSnapshotWindowTest extends ShellTestCase { createTaskDescription(Color.WHITE, Color.RED, Color.BLUE), 0 /* appearance */, windowFlags /* windowFlags */, 0 /* privateWindowFlags */, taskBounds, ORIENTATION_PORTRAIT, ACTIVITY_TYPE_STANDARD, - new InsetsState(), null /* clearWindow */, new TestShellExecutor()); + WindowInsets.Type.defaultVisible(), null /* clearWindow */, + new TestShellExecutor()); } private TaskSnapshot createTaskSnapshot(int width, int height, Point taskSize, diff --git a/libs/hwui/renderthread/CanvasContext.cpp b/libs/hwui/renderthread/CanvasContext.cpp index 6a0c5a8e499e..d09bc47cf8fd 100644 --- a/libs/hwui/renderthread/CanvasContext.cpp +++ b/libs/hwui/renderthread/CanvasContext.cpp @@ -472,11 +472,11 @@ void CanvasContext::notifyFramePending() { mRenderThread.pushBackFrameCallback(this); } -nsecs_t CanvasContext::draw() { +std::optional<nsecs_t> CanvasContext::draw() { if (auto grContext = getGrContext()) { if (grContext->abandoned()) { LOG_ALWAYS_FATAL("GrContext is abandoned/device lost at start of CanvasContext::draw"); - return 0; + return std::nullopt; } } SkRect dirty; @@ -498,7 +498,7 @@ nsecs_t CanvasContext::draw() { std::invoke(func, false /* didProduceBuffer */); } mFrameCommitCallbacks.clear(); - return 0; + return std::nullopt; } ScopedActiveContext activeContext(this); @@ -543,6 +543,8 @@ nsecs_t CanvasContext::draw() { } bool requireSwap = false; + bool didDraw = false; + int error = OK; bool didSwap = mRenderPipeline->swapBuffers(frame, drawResult.success, windowDirty, mCurrentFrameInfo, &requireSwap); @@ -553,7 +555,7 @@ nsecs_t CanvasContext::draw() { mIsDirty = false; if (requireSwap) { - bool didDraw = true; + didDraw = true; // Handle any swapchain errors error = mNativeSurface->getAndClearError(); if (error == TIMED_OUT) { @@ -649,7 +651,9 @@ nsecs_t CanvasContext::draw() { } mRenderThread.cacheManager().onFrameCompleted(); - return mCurrentFrameInfo->get(FrameInfoIndex::DequeueBufferDuration); + return didDraw ? std::make_optional( + mCurrentFrameInfo->get(FrameInfoIndex::DequeueBufferDuration)) + : std::nullopt; } void CanvasContext::reportMetricsWithPresentTime() { diff --git a/libs/hwui/renderthread/CanvasContext.h b/libs/hwui/renderthread/CanvasContext.h index 748ab96d7e06..db96cfb978e9 100644 --- a/libs/hwui/renderthread/CanvasContext.h +++ b/libs/hwui/renderthread/CanvasContext.h @@ -138,7 +138,7 @@ public: bool makeCurrent(); void prepareTree(TreeInfo& info, int64_t* uiFrameInfo, int64_t syncQueued, RenderNode* target); // Returns the DequeueBufferDuration. - nsecs_t draw(); + std::optional<nsecs_t> draw(); void destroy(); // IFrameCallback, Choreographer-driven frame callback entry point diff --git a/libs/hwui/renderthread/DrawFrameTask.cpp b/libs/hwui/renderthread/DrawFrameTask.cpp index 03f02de98efe..dc7676c08bd7 100644 --- a/libs/hwui/renderthread/DrawFrameTask.cpp +++ b/libs/hwui/renderthread/DrawFrameTask.cpp @@ -19,6 +19,7 @@ #include <dlfcn.h> #include <gui/TraceUtils.h> #include <utils/Log.h> + #include <algorithm> #include "../DeferredLayerUpdater.h" @@ -28,6 +29,7 @@ #include "CanvasContext.h" #include "RenderThread.h" #include "thread/CommonPool.h" +#include "utils/TimeUtils.h" namespace android { namespace uirenderer { @@ -146,6 +148,7 @@ void DrawFrameTask::run() { bool canUnblockUiThread; bool canDrawThisFrame; + bool didDraw = false; { TreeInfo info(TreeInfo::MODE_FULL, *mContext); info.forceDrawFrame = mForceDrawFrame; @@ -188,7 +191,9 @@ void DrawFrameTask::run() { nsecs_t dequeueBufferDuration = 0; if (CC_LIKELY(canDrawThisFrame)) { - dequeueBufferDuration = context->draw(); + std::optional<nsecs_t> drawResult = context->draw(); + didDraw = drawResult.has_value(); + dequeueBufferDuration = drawResult.value_or(0); } else { // Do a flush in case syncFrameState performed any texture uploads. Since we skipped // the draw() call, those uploads (or deletes) will end up sitting in the queue. @@ -209,8 +214,9 @@ void DrawFrameTask::run() { } if (!mHintSessionWrapper) mHintSessionWrapper.emplace(mUiThreadId, mRenderThreadId); - constexpr int64_t kSanityCheckLowerBound = 100000; // 0.1ms - constexpr int64_t kSanityCheckUpperBound = 10000000000; // 10s + + constexpr int64_t kSanityCheckLowerBound = 100_us; + constexpr int64_t kSanityCheckUpperBound = 10_s; int64_t targetWorkDuration = frameDeadline - intendedVsync; targetWorkDuration = targetWorkDuration * Properties::targetCpuTimePercentage / 100; if (targetWorkDuration > kSanityCheckLowerBound && @@ -219,12 +225,15 @@ void DrawFrameTask::run() { mLastTargetWorkDuration = targetWorkDuration; mHintSessionWrapper->updateTargetWorkDuration(targetWorkDuration); } - int64_t frameDuration = systemTime(SYSTEM_TIME_MONOTONIC) - frameStartTime; - int64_t actualDuration = frameDuration - - (std::min(syncDelayDuration, mLastDequeueBufferDuration)) - - dequeueBufferDuration; - if (actualDuration > kSanityCheckLowerBound && actualDuration < kSanityCheckUpperBound) { - mHintSessionWrapper->reportActualWorkDuration(actualDuration); + + if (didDraw) { + int64_t frameDuration = systemTime(SYSTEM_TIME_MONOTONIC) - frameStartTime; + int64_t actualDuration = frameDuration - + (std::min(syncDelayDuration, mLastDequeueBufferDuration)) - + dequeueBufferDuration; + if (actualDuration > kSanityCheckLowerBound && actualDuration < kSanityCheckUpperBound) { + mHintSessionWrapper->reportActualWorkDuration(actualDuration); + } } mLastDequeueBufferDuration = dequeueBufferDuration; diff --git a/location/java/android/location/GnssCapabilities.java b/location/java/android/location/GnssCapabilities.java index 7a412a0ed2de..b38f9ea39136 100644 --- a/location/java/android/location/GnssCapabilities.java +++ b/location/java/android/location/GnssCapabilities.java @@ -207,7 +207,7 @@ public final class GnssCapabilities implements Parcelable { /** * Returns {@code true} if GNSS chipset supports single shot locating, {@code false} otherwise. */ - public boolean hasSingleShot() { + public boolean hasSingleShotFix() { return (mTopFlags & TOP_HAL_CAPABILITY_SINGLE_SHOT) != 0; } @@ -482,7 +482,7 @@ public final class GnssCapabilities implements Parcelable { if (hasMsa()) { builder.append("MSA "); } - if (hasSingleShot()) { + if (hasSingleShotFix()) { builder.append("SINGLE_SHOT "); } if (hasOnDemandTime()) { @@ -602,7 +602,7 @@ public final class GnssCapabilities implements Parcelable { /** * Sets single shot locating capability. */ - public @NonNull Builder setHasSingleShot(boolean capable) { + public @NonNull Builder setHasSingleShotFix(boolean capable) { mTopFlags = setFlag(mTopFlags, TOP_HAL_CAPABILITY_SINGLE_SHOT, capable); return this; } diff --git a/location/java/android/location/GnssStatus.java b/location/java/android/location/GnssStatus.java index 23390fce1a5f..09f40e80f885 100644 --- a/location/java/android/location/GnssStatus.java +++ b/location/java/android/location/GnssStatus.java @@ -199,15 +199,15 @@ public final class GnssStatus implements Parcelable { * <li>93-106 as the frequency channel number (FCN) (-7 to +6) plus 100. * i.e. encode FCN of -7 as 93, 0 as 100, and +6 as 106</li> * </ul></li> - * <li>QZSS: 193-200</li> + * <li>QZSS: 183-206</li> * <li>Galileo: 1-36</li> - * <li>Beidou: 1-37</li> + * <li>Beidou: 1-63</li> * <li>IRNSS: 1-14</li> * </ul> * * @param satelliteIndex An index from zero to {@link #getSatelliteCount()} - 1 */ - @IntRange(from = 1, to = 200) + @IntRange(from = 1, to = 206) public int getSvid(@IntRange(from = 0) int satelliteIndex) { return mSvidWithFlags[satelliteIndex] >> SVID_SHIFT_WIDTH; } diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java index 26cb9f8e9ee1..b4b908dbff16 100644 --- a/media/java/android/media/MediaRouter2.java +++ b/media/java/android/media/MediaRouter2.java @@ -104,8 +104,8 @@ public final class MediaRouter2 { private final String mPackageName; /** - * Stores the latest copy of all routes received from {@link MediaRouter2ServiceImpl}, without - * any filtering, sorting, or deduplication. + * Stores the latest copy of all routes received from the system server, without any filtering, + * sorting, or deduplication. * * <p>Uses {@link MediaRoute2Info#getId()} to set each entry's key. */ diff --git a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SliceStoreActivity.java b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SliceStoreActivity.java index 24cb5f948474..602e31cbd130 100644 --- a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SliceStoreActivity.java +++ b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SliceStoreActivity.java @@ -85,7 +85,7 @@ public class SliceStoreActivity extends Activity { SliceStoreBroadcastReceiver.updateSliceStoreActivity(mCapability, this); - mWebView = new WebView(getApplicationContext()); + mWebView = new WebView(this); setContentView(mWebView); mWebView.loadUrl(mUrl.toString()); // TODO(b/245882601): Get back response from WebView diff --git a/packages/CompanionDeviceManager/res/values/styles.xml b/packages/CompanionDeviceManager/res/values/styles.xml index 428f2dc2eb35..2000d9675ca4 100644 --- a/packages/CompanionDeviceManager/res/values/styles.xml +++ b/packages/CompanionDeviceManager/res/values/styles.xml @@ -49,7 +49,6 @@ <style name="DescriptionSummary"> <item name="android:layout_width">match_parent</item> <item name="android:layout_height">wrap_content</item> - <item name="android:gravity">center</item> <item name="android:layout_marginTop">18dp</item> <item name="android:layout_marginLeft">18dp</item> <item name="android:layout_marginRight">18dp</item> diff --git a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt index e037db7aa0e2..2ba8748c16b7 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt @@ -35,7 +35,7 @@ class GetFlowUtils { ProviderInfo( // TODO: replace to extract from the service data structure when available icon = context.getDrawable(R.drawable.ic_passkey)!!, - name = it.providerId, + name = it.providerFlattenedComponentName, displayName = it.providerDisplayName, credentialTypeIcon = context.getDrawable(R.drawable.ic_passkey)!!, credentialOptions = toCredentialOptionInfoList(it.credentialEntries, context) @@ -79,7 +79,7 @@ class CreateFlowUtils { com.android.credentialmanager.createflow.ProviderInfo( // TODO: replace to extract from the service data structure when available icon = context.getDrawable(R.drawable.ic_passkey)!!, - name = it.providerId, + name = it.providerFlattenedComponentName, displayName = it.providerDisplayName, credentialTypeIcon = context.getDrawable(R.drawable.ic_passkey)!!, createOptions = toCreationOptionInfoList(it.credentialEntries, context), diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/ActionUi.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/ActionUi.kt new file mode 100644 index 000000000000..d4341b498fe0 --- /dev/null +++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/ActionUi.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.credentialmanager.jetpack + +import android.app.slice.Slice +import android.credentials.ui.Entry +import android.graphics.drawable.Icon + +/** + * UI representation for a credential entry used during the get credential flow. + * + * TODO: move to jetpack. + */ +class ActionUi( + val icon: Icon, + val text: CharSequence, + val subtext: CharSequence?, +) { + companion object { + fun fromSlice(slice: Slice): ActionUi { + var icon: Icon? = null + var text: CharSequence? = null + var subtext: CharSequence? = null + + val items = slice.items + items.forEach { + if (it.hasHint(Entry.HINT_ACTION_ICON)) { + icon = it.icon + } else if (it.hasHint(Entry.HINT_ACTION_TITLE)) { + text = it.text + } else if (it.hasHint(Entry.HINT_ACTION_SUBTEXT)) { + subtext = it.text + } + } + // TODO: fail NPE more elegantly. + return ActionUi(icon!!, text!!, subtext) + } + } +} diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryApplication.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryApplication.kt index 36b58ad794dc..dfbf244696bb 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryApplication.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryApplication.kt @@ -22,6 +22,6 @@ import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory class GalleryApplication : Application() { override fun onCreate() { super.onCreate() - SpaEnvironmentFactory.reset(GallerySpaEnvironment) + SpaEnvironmentFactory.reset(GallerySpaEnvironment(this)) } }
\ No newline at end of file diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt index 4af25893ea37..92f4fe49cd02 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt @@ -16,6 +16,7 @@ package com.android.settingslib.spa.gallery +import android.content.Context import com.android.settingslib.spa.framework.common.LocalLogger import com.android.settingslib.spa.framework.common.SettingsPageProviderRepository import com.android.settingslib.spa.framework.common.SpaEnvironment @@ -49,7 +50,7 @@ enum class SettingsPageProviderEnum(val displayName: String) { // Add your SPPs } -object GallerySpaEnvironment : SpaEnvironment() { +class GallerySpaEnvironment(context: Context) : SpaEnvironment(context) { override val pageProviderRepository = lazy { SettingsPageProviderRepository( allPageProviders = listOf( diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt index 7fd49db93748..83e3f786c200 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt @@ -22,6 +22,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.android.settingslib.spa.framework.common.SettingsEntry import com.android.settingslib.spa.framework.common.SettingsPageProvider +import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory import com.android.settingslib.spa.framework.common.createSettingsPage import com.android.settingslib.spa.framework.theme.SettingsTheme import com.android.settingslib.spa.gallery.R @@ -76,6 +77,7 @@ object HomePageProvider : SettingsPageProvider { @Preview(showBackground = true) @Composable private fun HomeScreenPreview() { + SpaEnvironmentFactory.resetForPreview() SettingsTheme { HomePageProvider.Page(null) } diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPage.kt index 82073104a3a7..60ff3627b33e 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPage.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPage.kt @@ -23,6 +23,7 @@ import com.android.settingslib.spa.framework.common.SettingsEntry import com.android.settingslib.spa.framework.common.SettingsEntryBuilder import com.android.settingslib.spa.framework.common.SettingsPage import com.android.settingslib.spa.framework.common.SettingsPageProvider +import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory import com.android.settingslib.spa.framework.common.createSettingsPage import com.android.settingslib.spa.framework.theme.SettingsTheme import com.android.settingslib.spa.gallery.SettingsPageProviderEnum @@ -115,6 +116,7 @@ object ArgumentPageProvider : SettingsPageProvider { @Preview(showBackground = true) @Composable private fun ArgumentPagePreview() { + SpaEnvironmentFactory.resetForPreview() SettingsTheme { ArgumentPageProvider.Page( ArgumentPageModel.buildArgument(stringParam = "foo", intParam = 0) diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePage.kt index fa8d51c3561f..2c2782b20e35 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePage.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePage.kt @@ -217,6 +217,7 @@ object PreferencePageProvider : SettingsPageProvider { @Preview(showBackground = true) @Composable private fun PreferencePagePreview() { + SpaEnvironmentFactory.resetForPreview() SettingsTheme { PreferencePageProvider.Page(null) } diff --git a/packages/SettingsLib/Spa/spa/build.gradle b/packages/SettingsLib/Spa/spa/build.gradle index 2820ed7bc96e..3b159e95cc55 100644 --- a/packages/SettingsLib/Spa/spa/build.gradle +++ b/packages/SettingsLib/Spa/spa/build.gradle @@ -59,9 +59,9 @@ dependencies { api "androidx.compose.material:material-icons-extended:$jetpack_compose_version" api "androidx.compose.runtime:runtime-livedata:$jetpack_compose_version" api "androidx.compose.ui:ui-tooling-preview:$jetpack_compose_version" - api "androidx.lifecycle:lifecycle-livedata-ktx:2.6.0-alpha02" + api "androidx.lifecycle:lifecycle-livedata-ktx:2.6.0-alpha03" api "androidx.navigation:navigation-compose:2.5.0" - api "com.google.android.material:material:1.6.1" + api "com.google.android.material:material:1.7.0-alpha03" debugApi "androidx.compose.ui:ui-tooling:$jetpack_compose_version" implementation "com.airbnb.android:lottie-compose:5.2.0" } diff --git a/packages/SettingsLib/Spa/spa/res/values-night/themes.xml b/packages/SettingsLib/Spa/spa/res/values-night/themes.xml deleted file mode 100644 index 67dd2b0cc5e0..000000000000 --- a/packages/SettingsLib/Spa/spa/res/values-night/themes.xml +++ /dev/null @@ -1,20 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - Copyright (C) 2022 The Android Open Source Project - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. ---> -<resources> - - <style name="Theme.SpaLib.DayNight" /> -</resources> diff --git a/packages/SettingsLib/Spa/spa/res/values/themes.xml b/packages/SettingsLib/Spa/spa/res/values/themes.xml index e0e5fc211ec6..25846ec2d20b 100644 --- a/packages/SettingsLib/Spa/spa/res/values/themes.xml +++ b/packages/SettingsLib/Spa/spa/res/values/themes.xml @@ -16,12 +16,10 @@ --> <resources> - <style name="Theme.SpaLib" parent="Theme.Material3.DayNight.NoActionBar"> + <style name="Theme.SpaLib" parent="@android:style/Theme.DeviceDefault.Settings"> <item name="android:statusBarColor">@android:color/transparent</item> <item name="android:navigationBarColor">@android:color/transparent</item> - </style> - - <style name="Theme.SpaLib.DayNight"> - <item name="android:windowLightStatusBar">true</item> + <item name="android:windowActionBar">false</item> + <item name="android:windowNoTitle">true</item> </style> </resources> diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt index d3efaa7480f8..c3c90ab4fdb8 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt @@ -27,6 +27,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.core.view.WindowCompat import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.navigation.NavGraph.Companion.findStartDestination @@ -66,8 +67,9 @@ open class BrowseActivity : ComponentActivity() { private val spaEnvironment get() = SpaEnvironmentFactory.instance override fun onCreate(savedInstanceState: Bundle?) { - setTheme(R.style.Theme_SpaLib_DayNight) + setTheme(R.style.Theme_SpaLib) super.onCreate(savedInstanceState) + WindowCompat.setDecorFitsSystemWindows(window, false) spaEnvironment.logger.message(TAG, "onCreate", category = LogCategory.FRAMEWORK) setContent { @@ -83,35 +85,19 @@ open class BrowseActivity : ComponentActivity() { val navController = rememberNavController() val nullPage = SettingsPage.createNull() CompositionLocalProvider(navController.localNavController()) { - NavHost(navController, nullPage.sppName) { + NavHost( + navController = navController, + startDestination = nullPage.sppName, + ) { composable(nullPage.sppName) {} for (spp in sppRepository.getAllProviders()) { composable( route = spp.name + spp.parameter.navRoute(), arguments = spp.parameter, ) { navBackStackEntry -> - val lifecycleOwner = LocalLifecycleOwner.current - val sp = remember(navBackStackEntry.arguments) { + PageLogger(remember(navBackStackEntry.arguments) { spp.createSettingsPage(arguments = navBackStackEntry.arguments) - } - - DisposableEffect(lifecycleOwner) { - val observer = LifecycleEventObserver { _, event -> - if (event == Lifecycle.Event.ON_START) { - sp.enterPage() - } else if (event == Lifecycle.Event.ON_STOP) { - sp.leavePage() - } - } - - // Add the observer to the lifecycle - lifecycleOwner.lifecycle.addObserver(observer) - - // When the effect leaves the Composition, remove the observer - onDispose { - lifecycleOwner.lifecycle.removeObserver(observer) - } - } + }) spp.Page(navBackStackEntry.arguments) } @@ -122,6 +108,28 @@ open class BrowseActivity : ComponentActivity() { } @Composable + private fun PageLogger(settingsPage: SettingsPage) { + val lifecycleOwner = LocalLifecycleOwner.current + DisposableEffect(lifecycleOwner) { + val observer = LifecycleEventObserver { _, event -> + if (event == Lifecycle.Event.ON_START) { + settingsPage.enterPage() + } else if (event == Lifecycle.Event.ON_STOP) { + settingsPage.leavePage() + } + } + + // Add the observer to the lifecycle + lifecycleOwner.lifecycle.addObserver(observer) + + // When the effect leaves the Composition, remove the observer + onDispose { + lifecycleOwner.lifecycle.removeObserver(observer) + } + } + } + + @Composable private fun InitialDestinationNavigator() { val sppRepository by spaEnvironment.pageProviderRepository val destinationNavigated = rememberSaveable { mutableStateOf(false) } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SpaEnvironment.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SpaEnvironment.kt index 5baee4fb4acc..60734254a58b 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SpaEnvironment.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SpaEnvironment.kt @@ -17,7 +17,10 @@ package com.android.settingslib.spa.framework.common import android.app.Activity +import android.content.Context import android.util.Log +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext private const val TAG = "SpaEnvironment" @@ -29,6 +32,20 @@ object SpaEnvironmentFactory { Log.d(TAG, "reset") } + @Composable + fun resetForPreview() { + val context = LocalContext.current + spaEnvironment = object : SpaEnvironment(context) { + override val pageProviderRepository = lazy { + SettingsPageProviderRepository( + allPageProviders = emptyList(), + rootPages = emptyList() + ) + } + } + Log.d(TAG, "resetForPreview") + } + val instance: SpaEnvironment get() { if (spaEnvironment == null) @@ -37,11 +54,13 @@ object SpaEnvironmentFactory { } } -abstract class SpaEnvironment { +abstract class SpaEnvironment(context: Context) { abstract val pageProviderRepository: Lazy<SettingsPageProviderRepository> val entryRepository = lazy { SettingsEntryRepository(pageProviderRepository.value) } + val appContext: Context = context.applicationContext + open val browseActivityClass: Class<out Activity>? = null open val entryProviderAuthorities: String? = null diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/PaddingValuesExt.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/PaddingValuesExt.kt new file mode 100644 index 000000000000..18335ff6eba5 --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/PaddingValuesExt.kt @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.spa.framework.compose + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp + +internal fun PaddingValues.horizontalValues(): PaddingValues = HorizontalPaddingValues(this) + +internal fun PaddingValues.verticalValues(): PaddingValues = VerticalPaddingValues(this) + +private class HorizontalPaddingValues(private val paddingValues: PaddingValues) : PaddingValues { + override fun calculateLeftPadding(layoutDirection: LayoutDirection) = + paddingValues.calculateLeftPadding(layoutDirection) + + override fun calculateTopPadding(): Dp = 0.dp + + override fun calculateRightPadding(layoutDirection: LayoutDirection) = + paddingValues.calculateRightPadding(layoutDirection) + + override fun calculateBottomPadding() = 0.dp +} + +private class VerticalPaddingValues(private val paddingValues: PaddingValues) : PaddingValues { + override fun calculateLeftPadding(layoutDirection: LayoutDirection) = 0.dp + + override fun calculateTopPadding(): Dp = paddingValues.calculateTopPadding() + + override fun calculateRightPadding(layoutDirection: LayoutDirection) = 0.dp + + override fun calculateBottomPadding() = paddingValues.calculateBottomPadding() +} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/debug/DebugActivity.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/debug/DebugActivity.kt index 9eaa88ae3168..26491d51e838 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/debug/DebugActivity.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/debug/DebugActivity.kt @@ -62,7 +62,7 @@ class DebugActivity : ComponentActivity() { private val spaEnvironment get() = SpaEnvironmentFactory.instance override fun onCreate(savedInstanceState: Bundle?) { - setTheme(R.style.Theme_SpaLib_DayNight) + setTheme(R.style.Theme_SpaLib) super.onCreate(savedInstanceState) spaEnvironment.logger.message(TAG, "onCreate", category = LogCategory.FRAMEWORK) diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/HomeScaffold.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/HomeScaffold.kt index eb20ac5c5f09..711c8a753532 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/HomeScaffold.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/HomeScaffold.kt @@ -20,6 +20,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.MaterialTheme @@ -34,6 +35,7 @@ fun HomeScaffold(title: String, content: @Composable () -> Unit) { Modifier .fillMaxSize() .background(color = MaterialTheme.colorScheme.background) + .systemBarsPadding() .verticalScroll(rememberScrollState()), ) { Text( diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/RegularScaffold.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/RegularScaffold.kt index 9a17b2a8cb78..d17a8dcd0161 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/RegularScaffold.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/RegularScaffold.kt @@ -19,7 +19,7 @@ package com.android.settingslib.spa.widget.scaffold import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.height import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Scaffold @@ -27,6 +27,8 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import com.android.settingslib.spa.framework.theme.SettingsTheme +import com.android.settingslib.spa.widget.preference.Preference +import com.android.settingslib.spa.widget.preference.PreferenceModel /** * A [Scaffold] which content is scrollable and wrapped in a [Column]. @@ -42,8 +44,9 @@ fun RegularScaffold( ) { SettingsScaffold(title, actions) { paddingValues -> Column(Modifier.verticalScroll(rememberScrollState())) { - Spacer(Modifier.padding(paddingValues)) + Spacer(Modifier.height(paddingValues.calculateTopPadding())) content() + Spacer(Modifier.height(paddingValues.calculateBottomPadding())) } } } @@ -52,6 +55,13 @@ fun RegularScaffold( @Composable private fun RegularScaffoldPreview() { SettingsTheme { - RegularScaffold(title = "Display") {} + RegularScaffold(title = "Display") { + Preference(object : PreferenceModel { + override val title = "Item 1" + }) + Preference(object : PreferenceModel { + override val title = "Item 2" + }) + } } } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SearchScaffold.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SearchScaffold.kt index 4f83ad6bd291..efc623af9cc0 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SearchScaffold.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SearchScaffold.kt @@ -14,13 +14,12 @@ * limitations under the License. */ -@file:OptIn(ExperimentalMaterial3Api::class) - package com.android.settingslib.spa.widget.scaffold import androidx.activity.compose.BackHandler import androidx.appcompat.R import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -31,10 +30,13 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.State @@ -48,45 +50,57 @@ import androidx.compose.ui.draw.alpha import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp import androidx.lifecycle.ViewModel import androidx.lifecycle.viewmodel.compose.viewModel import com.android.settingslib.spa.framework.compose.hideKeyboardAction +import com.android.settingslib.spa.framework.compose.horizontalValues import com.android.settingslib.spa.framework.theme.SettingsOpacity import com.android.settingslib.spa.framework.theme.SettingsTheme +import com.android.settingslib.spa.widget.preference.Preference +import com.android.settingslib.spa.widget.preference.PreferenceModel /** * A [Scaffold] which content is can be full screen, and with a search feature built-in. */ +@OptIn(ExperimentalMaterial3Api::class) @Composable fun SearchScaffold( title: String, actions: @Composable RowScope.() -> Unit = {}, - content: @Composable (searchQuery: State<String>) -> Unit, + content: @Composable (bottomPadding: Dp, searchQuery: State<String>) -> Unit, ) { val viewModel: SearchScaffoldViewModel = viewModel() + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() Scaffold( + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topBar = { SearchableTopAppBar( title = title, actions = actions, + scrollBehavior = scrollBehavior, searchQuery = viewModel.searchQuery, ) { viewModel.searchQuery = it } }, ) { paddingValues -> Box( Modifier - .padding(paddingValues) - .fillMaxSize() + .padding(paddingValues.horizontalValues()) + .padding(top = paddingValues.calculateTopPadding()) + .fillMaxSize(), ) { - val searchQuery = remember { - derivedStateOf { viewModel.searchQuery?.text ?: "" } - } - content(searchQuery) + content( + bottomPadding = paddingValues.calculateBottomPadding(), + searchQuery = remember { + derivedStateOf { viewModel.searchQuery?.text ?: "" } + }, + ) } } } @@ -95,10 +109,12 @@ internal class SearchScaffoldViewModel : ViewModel() { var searchQuery: TextFieldValue? by mutableStateOf(null) } +@OptIn(ExperimentalMaterial3Api::class) @Composable private fun SearchableTopAppBar( title: String, actions: @Composable RowScope.() -> Unit, + scrollBehavior: TopAppBarScrollBehavior, searchQuery: TextFieldValue?, onSearchQueryChange: (TextFieldValue?) -> Unit, ) { @@ -110,13 +126,17 @@ private fun SearchableTopAppBar( actions = actions, ) } else { - SettingsTopAppBar(title) { - SearchAction { onSearchQueryChange(TextFieldValue()) } + SettingsTopAppBar(title, scrollBehavior) { + SearchAction { + scrollBehavior.collapse() + onSearchQueryChange(TextFieldValue()) + } actions() } } } +@OptIn(ExperimentalMaterial3Api::class) @Composable private fun SearchTopAppBar( query: TextFieldValue, @@ -124,21 +144,24 @@ private fun SearchTopAppBar( onClose: () -> Unit, actions: @Composable RowScope.() -> Unit = {}, ) { - TopAppBar( - title = { SearchBox(query, onQueryChange) }, - modifier = Modifier.statusBarsPadding(), - navigationIcon = { CollapseAction(onClose) }, - actions = { - if (query.text.isNotEmpty()) { - ClearAction { onQueryChange(TextFieldValue()) } - } - actions() - }, - colors = settingsTopAppBarColors(), - ) + Surface(color = SettingsTheme.colorScheme.surfaceHeader) { + TopAppBar( + title = { SearchBox(query, onQueryChange) }, + modifier = Modifier.statusBarsPadding(), + navigationIcon = { CollapseAction(onClose) }, + actions = { + if (query.text.isNotEmpty()) { + ClearAction { onQueryChange(TextFieldValue()) } + } + actions() + }, + colors = TopAppBarDefaults.smallTopAppBarColors(containerColor = Color.Transparent), + ) + } BackHandler { onClose() } } +@OptIn(ExperimentalMaterial3Api::class) @Composable private fun SearchBox(query: TextFieldValue, onQueryChange: (TextFieldValue) -> Unit) { val focusRequester = remember { FocusRequester() } @@ -184,6 +207,15 @@ private fun SearchTopAppBarPreview() { @Composable private fun SearchScaffoldPreview() { SettingsTheme { - SearchScaffold(title = "App notifications") {} + SearchScaffold(title = "App notifications") { _, _ -> + Column { + Preference(object : PreferenceModel { + override val title = "Item 1" + }) + Preference(object : PreferenceModel { + override val title = "Item 2" + }) + } + } } } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsScaffold.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsScaffold.kt index 3bc3dd72d353..f4e504a954a2 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsScaffold.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsScaffold.kt @@ -16,13 +16,23 @@ package com.android.settingslib.spa.widget.scaffold +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Scaffold +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.tooling.preview.Preview +import com.android.settingslib.spa.framework.compose.horizontalValues +import com.android.settingslib.spa.framework.compose.verticalValues import com.android.settingslib.spa.framework.theme.SettingsTheme +import com.android.settingslib.spa.widget.preference.Preference +import com.android.settingslib.spa.widget.preference.PreferenceModel /** * A [Scaffold] which content is can be full screen when needed. @@ -34,16 +44,30 @@ fun SettingsScaffold( actions: @Composable RowScope.() -> Unit = {}, content: @Composable (PaddingValues) -> Unit, ) { + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() Scaffold( - topBar = { SettingsTopAppBar(title, actions) }, - content = content, - ) + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { SettingsTopAppBar(title, scrollBehavior, actions) }, + ) { paddingValues -> + Box(Modifier.padding(paddingValues.horizontalValues())) { + content(paddingValues.verticalValues()) + } + } } @Preview @Composable private fun SettingsScaffoldPreview() { SettingsTheme { - SettingsScaffold(title = "Display") {} + SettingsScaffold(title = "Display") { paddingValues -> + Column(Modifier.padding(paddingValues)) { + Preference(object : PreferenceModel { + override val title = "Item 1" + }) + Preference(object : PreferenceModel { + override val title = "Item 2" + }) + } + } } } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsTopAppBar.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsTopAppBar.kt index 93535203b1b9..f7cb035cbf93 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsTopAppBar.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsTopAppBar.kt @@ -17,41 +17,70 @@ package com.android.settingslib.spa.widget.scaffold import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.LargeTopAppBar +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextOverflow +import com.android.settingslib.spa.framework.compose.horizontalValues import com.android.settingslib.spa.framework.theme.SettingsDimension import com.android.settingslib.spa.framework.theme.SettingsTheme +import com.android.settingslib.spa.framework.theme.rememberSettingsTypography @OptIn(ExperimentalMaterial3Api::class) @Composable internal fun SettingsTopAppBar( title: String, + scrollBehavior: TopAppBarScrollBehavior, actions: @Composable RowScope.() -> Unit, ) { - TopAppBar( - title = { - Text( - text = title, - modifier = Modifier.padding(SettingsDimension.itemPaddingAround), - overflow = TextOverflow.Ellipsis, - maxLines = 1, - ) - }, - navigationIcon = { NavigateBack() }, - actions = actions, - colors = settingsTopAppBarColors(), + val colorScheme = MaterialTheme.colorScheme + // TODO: Remove MaterialTheme() after top app bar color fixed in AndroidX. + MaterialTheme( + colorScheme = remember { colorScheme.copy(surface = colorScheme.background) }, + typography = rememberSettingsTypography(), + ) { + LargeTopAppBar( + title = { Title(title) }, + navigationIcon = { NavigateBack() }, + actions = actions, + colors = largeTopAppBarColors(), + scrollBehavior = scrollBehavior, + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +internal fun TopAppBarScrollBehavior.collapse() { + with(state) { + heightOffset = heightOffsetLimit + } +} + +@Composable +private fun Title(title: String) { + Text( + text = title, + modifier = Modifier + .padding(WindowInsets.navigationBars.asPaddingValues().horizontalValues()) + .padding(SettingsDimension.itemPaddingAround), + overflow = TextOverflow.Ellipsis, + maxLines = 1, ) } @OptIn(ExperimentalMaterial3Api::class) @Composable -internal fun settingsTopAppBarColors() = TopAppBarDefaults.smallTopAppBarColors( - containerColor = SettingsTheme.colorScheme.surfaceHeader, +private fun largeTopAppBarColors() = TopAppBarDefaults.largeTopAppBarColors( + containerColor = MaterialTheme.colorScheme.background, scrolledContainerColor = SettingsTheme.colorScheme.surfaceHeader, ) diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/util/EntryHighlight.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/util/EntryHighlight.kt index 652e54de5e39..e26bdf76e9d6 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/util/EntryHighlight.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/util/EntryHighlight.kt @@ -16,21 +16,43 @@ package com.android.settingslib.spa.widget.util +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.repeatable +import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import com.android.settingslib.spa.framework.common.LocalEntryDataProvider +import com.android.settingslib.spa.framework.theme.SettingsTheme @Composable internal fun EntryHighlight(UiLayoutFn: @Composable () -> Unit) { val entryData = LocalEntryDataProvider.current - val isHighlighted = rememberSaveable { entryData.isHighlighted } - val backgroundColor = - if (isHighlighted) MaterialTheme.colorScheme.surfaceVariant else Color.Transparent + val entryIsHighlighted = rememberSaveable { entryData.isHighlighted } + var localHighlighted by rememberSaveable { mutableStateOf(false) } + SideEffect { + localHighlighted = entryIsHighlighted + } + + val backgroundColor by animateColorAsState( + targetValue = when { + localHighlighted -> MaterialTheme.colorScheme.surfaceVariant + else -> SettingsTheme.colorScheme.background + }, + animationSpec = repeatable( + iterations = 3, + animation = tween(durationMillis = 500), + repeatMode = RepeatMode.Restart + ) + ) Box(modifier = Modifier.background(color = backgroundColor)) { UiLayoutFn() } diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/RegularScaffoldTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/RegularScaffoldTest.kt new file mode 100644 index 000000000000..1964c436d138 --- /dev/null +++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/RegularScaffoldTest.kt @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.spa.widget.scaffold + +import androidx.compose.material3.Text +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class RegularScaffoldTest { + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun regularScaffold_titleIsDisplayed() { + composeTestRule.setContent { + RegularScaffold(title = TITLE) { + Text(text = "AAA") + Text(text = "BBB") + } + } + + composeTestRule.onNodeWithText(TITLE).assertIsDisplayed() + } + + @Test + fun regularScaffold_itemsAreDisplayed() { + composeTestRule.setContent { + RegularScaffold(title = TITLE) { + Text(text = "AAA") + Text(text = "BBB") + } + } + + composeTestRule.onNodeWithText("AAA").assertIsDisplayed() + composeTestRule.onNodeWithText("BBB").assertIsDisplayed() + } + + private companion object { + const val TITLE = "title" + } +} diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/SearchScaffoldTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/SearchScaffoldTest.kt index ec3379dd46ee..c3e1d544a6ab 100644 --- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/SearchScaffoldTest.kt +++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/SearchScaffoldTest.kt @@ -43,7 +43,7 @@ class SearchScaffoldTest { @Test fun initialState_titleIsDisplayed() { composeTestRule.setContent { - SearchScaffold(title = TITLE) {} + SearchScaffold(title = TITLE) { _, _ -> } } composeTestRule.onNodeWithText(TITLE).assertIsDisplayed() @@ -116,7 +116,7 @@ class SearchScaffoldTest { private fun setContent(): State<String> { lateinit var actualSearchQuery: State<String> composeTestRule.setContent { - SearchScaffold(title = TITLE) { searchQuery -> + SearchScaffold(title = TITLE) { _, searchQuery -> SideEffect { actualSearchQuery = searchQuery } diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/SettingsPagerKtTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/SettingsPagerTest.kt index 0c84eac45cb7..0c745d5d5b3d 100644 --- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/SettingsPagerKtTest.kt +++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/SettingsPagerTest.kt @@ -16,7 +16,6 @@ package com.android.settingslib.spa.widget.scaffold -import androidx.compose.runtime.Composable import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsNotDisplayed import androidx.compose.ui.test.assertIsNotSelected @@ -31,15 +30,13 @@ import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) -class SettingsPagerKtTest { +class SettingsPagerTest { @get:Rule val composeTestRule = createComposeRule() @Test fun twoPage_initialState() { - composeTestRule.setContent { - TestTwoPage() - } + setTwoPagesContent() composeTestRule.onNodeWithText("Personal").assertIsSelected() composeTestRule.onNodeWithText("Page 0").assertIsDisplayed() @@ -49,9 +46,7 @@ class SettingsPagerKtTest { @Test fun twoPage_afterSwitch() { - composeTestRule.setContent { - TestTwoPage() - } + setTwoPagesContent() composeTestRule.onNodeWithText("Work").performClick() @@ -73,11 +68,12 @@ class SettingsPagerKtTest { composeTestRule.onNodeWithText("Page 0").assertIsDisplayed() composeTestRule.onNodeWithText("Page 1").assertDoesNotExist() } -} -@Composable -private fun TestTwoPage() { - SettingsPager(listOf("Personal", "Work")) { - SettingsTitle(title = "Page $it") + private fun setTwoPagesContent() { + composeTestRule.setContent { + SettingsPager(listOf("Personal", "Work")) { + SettingsTitle(title = "Page $it") + } + } } } diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/SettingsScaffoldTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/SettingsScaffoldTest.kt new file mode 100644 index 000000000000..f04240485386 --- /dev/null +++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/SettingsScaffoldTest.kt @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.spa.widget.scaffold + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.material3.Text +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class SettingsScaffoldTest { + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun settingsScaffold_titleIsDisplayed() { + composeTestRule.setContent { + SettingsScaffold(title = TITLE) { + Text(text = "AAA") + Text(text = "BBB") + } + } + + composeTestRule.onNodeWithText(TITLE).assertIsDisplayed() + } + + @Test + fun settingsScaffold_itemsAreDisplayed() { + composeTestRule.setContent { + SettingsScaffold(title = TITLE) { + Text(text = "AAA") + Text(text = "BBB") + } + } + + composeTestRule.onNodeWithText("AAA").assertIsDisplayed() + composeTestRule.onNodeWithText("BBB").assertIsDisplayed() + } + + @Test + fun settingsScaffold_noHorizontalPadding() { + lateinit var actualPaddingValues: PaddingValues + + composeTestRule.setContent { + SettingsScaffold(title = TITLE) { paddingValues -> + SideEffect { + actualPaddingValues = paddingValues + } + } + } + + assertThat(actualPaddingValues.calculateLeftPadding(LayoutDirection.Ltr)).isEqualTo(0.dp) + assertThat(actualPaddingValues.calculateLeftPadding(LayoutDirection.Rtl)).isEqualTo(0.dp) + assertThat(actualPaddingValues.calculateRightPadding(LayoutDirection.Ltr)).isEqualTo(0.dp) + assertThat(actualPaddingValues.calculateRightPadding(LayoutDirection.Rtl)).isEqualTo(0.dp) + } + + private companion object { + const val TITLE = "title" + } +} diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/ApplicationInfos.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/ApplicationInfos.kt index c1ac5d473dba..8954d22f45cc 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/ApplicationInfos.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/ApplicationInfos.kt @@ -35,6 +35,9 @@ val ApplicationInfo.userHandle: UserHandle /** Checks whether a flag is associated with the application. */ fun ApplicationInfo.hasFlag(flag: Int): Boolean = (flags and flag) > 0 +/** Checks whether the application is currently installed. */ +val ApplicationInfo.installed: Boolean get() = hasFlag(ApplicationInfo.FLAG_INSTALLED) + /** Checks whether the application is disabled until used. */ val ApplicationInfo.isDisabledUntilUsed: Boolean get() = enabledSetting == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/PackageManagerExt.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/PackageManagerExt.kt new file mode 100644 index 000000000000..2b2f11c7e23f --- /dev/null +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/PackageManagerExt.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.spaprivileged.model.app + +import android.content.Intent +import android.content.pm.ActivityInfo +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.content.pm.PackageManager.ResolveInfoFlags + +/** + * Checks if a package is system module. + */ +fun PackageManager.isSystemModule(packageName: String): Boolean = try { + getModuleInfo(packageName, 0) + true +} catch (_: PackageManager.NameNotFoundException) { + // Expected, not system module + false +} + +/** + * Resolves the activity to start for a given application and action. + */ +fun PackageManager.resolveActionForApp( + app: ApplicationInfo, + action: String, + flags: Int = 0, +): ActivityInfo? { + val intent = Intent(action).apply { + `package` = app.packageName + } + return resolveActivityAsUser(intent, ResolveInfoFlags.of(flags.toLong()), app.userId) + ?.activityInfo +} diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt index 408b9df5e3ef..3cd8378b8960 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt @@ -25,12 +25,12 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.Dp import androidx.lifecycle.viewmodel.compose.viewModel import com.android.settingslib.spa.framework.compose.LogCompositions import com.android.settingslib.spa.framework.compose.TimeMeasurer.Companion.rememberTimeMeasurer import com.android.settingslib.spa.framework.compose.rememberLazyListStateAndHideKeyboardWhenStartScroll import com.android.settingslib.spa.framework.compose.toState -import com.android.settingslib.spa.framework.theme.SettingsDimension import com.android.settingslib.spa.widget.ui.PlaceholderTitle import com.android.settingslib.spaprivileged.R import com.android.settingslib.spaprivileged.model.app.AppListConfig @@ -55,10 +55,11 @@ internal fun <T : AppRecord> AppList( option: State<Int>, searchQuery: State<String>, appItem: @Composable (itemState: AppListItemModel<T>) -> Unit, + bottomPadding: Dp, ) { LogCompositions(TAG, appListConfig.userId.toString()) val appListData = loadAppEntries(appListConfig, listModel, showSystem, option, searchQuery) - AppListWidget(appListData, listModel, appItem) + AppListWidget(appListData, listModel, appItem, bottomPadding) } @Composable @@ -66,6 +67,7 @@ private fun <T : AppRecord> AppListWidget( appListData: State<AppListData<T>?>, listModel: AppListModel<T>, appItem: @Composable (itemState: AppListItemModel<T>) -> Unit, + bottomPadding: Dp, ) { val timeMeasurer = rememberTimeMeasurer(TAG) appListData.value?.let { (list, option) -> @@ -77,7 +79,7 @@ private fun <T : AppRecord> AppListWidget( LazyColumn( modifier = Modifier.fillMaxSize(), state = rememberLazyListStateAndHideKeyboardWhenStartScroll(), - contentPadding = PaddingValues(bottom = SettingsDimension.itemPaddingVertical), + contentPadding = PaddingValues(bottom = bottomPadding), ) { items(count = list.size, key = { option to list[it].record.app.packageName }) { val appEntry = list[it] diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListPage.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListPage.kt index 99376b0005e4..29533679d9c1 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListPage.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListPage.kt @@ -52,7 +52,7 @@ fun <T : AppRecord> AppListPage( actions = { ShowSystemAction(showSystem.value) { showSystem.value = it } }, - ) { searchQuery -> + ) { bottomPadding, searchQuery -> WorkProfilePager(primaryUserOnly) { userInfo -> Column(Modifier.fillMaxSize()) { val options = remember { listModel.getSpinnerOptions() } @@ -68,6 +68,7 @@ fun <T : AppRecord> AppListPage( option = selectedOption, searchQuery = searchQuery, appItem = appItem, + bottomPadding = bottomPadding, ) } } diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/PackageManagerExtTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/PackageManagerExtTest.kt new file mode 100644 index 000000000000..4207490cba80 --- /dev/null +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/PackageManagerExtTest.kt @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.spaprivileged.model.app + +import android.content.ComponentName +import android.content.pm.ActivityInfo +import android.content.pm.ApplicationInfo +import android.content.pm.ModuleInfo +import android.content.pm.PackageManager +import android.content.pm.PackageManager.NameNotFoundException +import android.content.pm.PackageManager.ResolveInfoFlags +import android.content.pm.ResolveInfo +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.Mock +import org.mockito.Mockito.any +import org.mockito.Mockito.eq +import org.mockito.Mockito.verify +import org.mockito.junit.MockitoJUnit +import org.mockito.junit.MockitoRule +import org.mockito.Mockito.`when` as whenever + +@RunWith(AndroidJUnit4::class) +class PackageManagerExtTest { + @JvmField + @Rule + val mockito: MockitoRule = MockitoJUnit.rule() + + @Mock + private lateinit var packageManager: PackageManager + + private fun mockResolveActivityAsUser(resolveInfo: ResolveInfo?) { + whenever( + packageManager.resolveActivityAsUser(any(), any<ResolveInfoFlags>(), eq(APP.userId)) + ).thenReturn(resolveInfo) + } + + @Test + fun isSystemModule_whenSystemModule_returnTrue() { + whenever(packageManager.getModuleInfo(PACKAGE_NAME, 0)).thenReturn(ModuleInfo()) + + val isSystemModule = packageManager.isSystemModule(PACKAGE_NAME) + + assertThat(isSystemModule).isTrue() + } + + @Test + fun isSystemModule_whenNotSystemModule_returnFalse() { + whenever(packageManager.getModuleInfo(PACKAGE_NAME, 0)).thenThrow(NameNotFoundException()) + + val isSystemModule = packageManager.isSystemModule(PACKAGE_NAME) + + assertThat(isSystemModule).isFalse() + } + + @Test + fun resolveActionForApp_noResolveInfo() { + mockResolveActivityAsUser(null) + + val activityInfo = packageManager.resolveActionForApp(APP, ACTION) + + assertThat(activityInfo).isNull() + } + + @Test + fun resolveActionForApp_noActivityInfo() { + mockResolveActivityAsUser(ResolveInfo()) + + val activityInfo = packageManager.resolveActionForApp(APP, ACTION) + + assertThat(activityInfo).isNull() + } + + @Test + fun resolveActionForApp_hasActivityInfo() { + mockResolveActivityAsUser(ResolveInfo().apply { + activityInfo = ActivityInfo().apply { + packageName = PACKAGE_NAME + name = ACTIVITY_NAME + } + }) + + val activityInfo = packageManager.resolveActionForApp(APP, ACTION)!! + + assertThat(activityInfo.componentName).isEqualTo(ComponentName(PACKAGE_NAME, ACTIVITY_NAME)) + } + + @Test + fun resolveActionForApp_withFlags() { + packageManager.resolveActionForApp( + app = APP, + action = ACTION, + flags = PackageManager.GET_META_DATA, + ) + + val flagsCaptor = ArgumentCaptor.forClass(ResolveInfoFlags::class.java) + verify(packageManager).resolveActivityAsUser(any(), flagsCaptor.capture(), eq(APP.userId)) + assertThat(flagsCaptor.value.value).isEqualTo(PackageManager.GET_META_DATA.toLong()) + } + + private companion object { + const val PACKAGE_NAME = "package.name" + const val ACTIVITY_NAME = "ActivityName" + const val ACTION = "action" + const val UID = 123 + val APP = ApplicationInfo().apply { + packageName = PACKAGE_NAME + uid = UID + } + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java index eb53ea1d44f7..950ee21ae7b5 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java @@ -758,23 +758,16 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> } public boolean isBusy() { - for (CachedBluetoothDevice memberDevice : getMemberDevice()) { - if (isBusyState(memberDevice)) { - return true; - } - } - return isBusyState(this); - } - - private boolean isBusyState(CachedBluetoothDevice device){ - for (LocalBluetoothProfile profile : device.getProfiles()) { - int status = device.getProfileConnectionState(profile); - if (status == BluetoothProfile.STATE_CONNECTING - || status == BluetoothProfile.STATE_DISCONNECTING) { - return true; + synchronized (mProfileLock) { + for (LocalBluetoothProfile profile : mProfiles) { + int status = getProfileConnectionState(profile); + if (status == BluetoothProfile.STATE_CONNECTING + || status == BluetoothProfile.STATE_DISCONNECTING) { + return true; + } } + return getBondState() == BluetoothDevice.BOND_BONDING; } - return device.getBondState() == BluetoothDevice.BOND_BONDING; } private boolean updateProfiles() { @@ -920,7 +913,14 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> @Override public String toString() { - return mDevice.toString(); + return "CachedBluetoothDevice (" + + "anonymizedAddress=" + + mDevice.getAnonymizedAddress() + + ", name=" + + getName() + + ", groupId=" + + mGroupId + + ")"; } @Override diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java index 8a9f9dd9c3fb..fb861da0a7f0 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java @@ -231,7 +231,7 @@ public class LocalBluetoothProfileManager { if (DEBUG) { Log.d(TAG, "Adding local Volume Control profile"); } - mVolumeControlProfile = new VolumeControlProfile(); + mVolumeControlProfile = new VolumeControlProfile(mContext, mDeviceManager, this); // Note: no event handler for VCP, only for being connectable. mProfileNameMap.put(VolumeControlProfile.NAME, mVolumeControlProfile); } @@ -553,6 +553,10 @@ public class LocalBluetoothProfileManager { return mCsipSetCoordinatorProfile; } + public VolumeControlProfile getVolumeControlProfile() { + return mVolumeControlProfile; + } + /** * Fill in a list of LocalBluetoothProfile objects that are supported by * the local device and the remote device. diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/VolumeControlProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/VolumeControlProfile.java index 511df282ea4b..57867be53bb9 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/VolumeControlProfile.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/VolumeControlProfile.java @@ -16,18 +16,91 @@ package com.android.settingslib.bluetooth; +import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED; +import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN; + +import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothClass; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothProfile; +import android.bluetooth.BluetoothVolumeControl; +import android.content.Context; +import android.os.Build; +import android.util.Log; + +import java.util.ArrayList; +import java.util.List; + +import androidx.annotation.RequiresApi; /** * VolumeControlProfile handles Bluetooth Volume Control Controller role */ public class VolumeControlProfile implements LocalBluetoothProfile { private static final String TAG = "VolumeControlProfile"; + private static boolean DEBUG = true; static final String NAME = "VCP"; // Order of this profile in device profiles list - private static final int ORDINAL = 23; + private static final int ORDINAL = 1; + + private Context mContext; + private final CachedBluetoothDeviceManager mDeviceManager; + private final LocalBluetoothProfileManager mProfileManager; + + private BluetoothVolumeControl mService; + private boolean mIsProfileReady; + + // These callbacks run on the main thread. + private final class VolumeControlProfileServiceListener + implements BluetoothProfile.ServiceListener { + + @RequiresApi(Build.VERSION_CODES.S) + public void onServiceConnected(int profile, BluetoothProfile proxy) { + if (DEBUG) { + Log.d(TAG, "Bluetooth service connected"); + } + mService = (BluetoothVolumeControl) proxy; + // We just bound to the service, so refresh the UI for any connected + // VolumeControlProfile devices. + List<BluetoothDevice> deviceList = mService.getConnectedDevices(); + while (!deviceList.isEmpty()) { + BluetoothDevice nextDevice = deviceList.remove(0); + CachedBluetoothDevice device = mDeviceManager.findDevice(nextDevice); + // we may add a new device here, but generally this should not happen + if (device == null) { + if (DEBUG) { + Log.d(TAG, "VolumeControlProfile found new device: " + nextDevice); + } + device = mDeviceManager.addDevice(nextDevice); + } + device.onProfileStateChanged(VolumeControlProfile.this, + BluetoothProfile.STATE_CONNECTED); + device.refresh(); + } + + mProfileManager.callServiceConnectedListeners(); + mIsProfileReady = true; + } + + public void onServiceDisconnected(int profile) { + if (DEBUG) { + Log.d(TAG, "Bluetooth service disconnected"); + } + mProfileManager.callServiceDisconnectedListeners(); + mIsProfileReady = false; + } + } + + VolumeControlProfile(Context context, CachedBluetoothDeviceManager deviceManager, + LocalBluetoothProfileManager profileManager) { + mContext = context; + mDeviceManager = deviceManager; + mProfileManager = profileManager; + + BluetoothAdapter.getDefaultAdapter().getProfileProxy(context, + new VolumeControlProfile.VolumeControlProfileServiceListener(), + BluetoothProfile.VOLUME_CONTROL); + } @Override public boolean accessProfileEnabled() { @@ -39,29 +112,70 @@ public class VolumeControlProfile implements LocalBluetoothProfile { return true; } + /** + * Get VolumeControlProfile devices matching connection states{ + * + * @return Matching device list + * @code BluetoothProfile.STATE_CONNECTED, + * @code BluetoothProfile.STATE_CONNECTING, + * @code BluetoothProfile.STATE_DISCONNECTING} + */ + public List<BluetoothDevice> getConnectedDevices() { + if (mService == null) { + return new ArrayList<BluetoothDevice>(0); + } + return mService.getDevicesMatchingConnectionStates( + new int[]{BluetoothProfile.STATE_CONNECTED, BluetoothProfile.STATE_CONNECTING, + BluetoothProfile.STATE_DISCONNECTING}); + } + @Override public int getConnectionStatus(BluetoothDevice device) { - return BluetoothProfile.STATE_DISCONNECTED; // Settings app doesn't handle VCP + if (mService == null) { + return BluetoothProfile.STATE_DISCONNECTED; + } + return mService.getConnectionState(device); } @Override public boolean isEnabled(BluetoothDevice device) { - return false; + if (mService == null || device == null) { + return false; + } + return mService.getConnectionPolicy(device) > CONNECTION_POLICY_FORBIDDEN; } @Override public int getConnectionPolicy(BluetoothDevice device) { - return BluetoothProfile.CONNECTION_POLICY_FORBIDDEN; // Settings app doesn't handle VCP + if (mService == null || device == null) { + return CONNECTION_POLICY_FORBIDDEN; + } + return mService.getConnectionPolicy(device); } @Override public boolean setEnabled(BluetoothDevice device, boolean enabled) { - return false; + boolean isSuccessful = false; + if (mService == null || device == null) { + return false; + } + if (DEBUG) { + Log.d(TAG, device.getAnonymizedAddress() + " setEnabled: " + enabled); + } + if (enabled) { + if (mService.getConnectionPolicy(device) < CONNECTION_POLICY_ALLOWED) { + isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_ALLOWED); + } + } else { + isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_FORBIDDEN); + } + + return isSuccessful; } @Override public boolean isProfileReady() { - return true; + return mIsProfileReady; } @Override diff --git a/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java b/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java index 1606540da3fd..2614644feb20 100644 --- a/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java +++ b/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java @@ -92,7 +92,8 @@ public class DreamBackend { COMPLICATION_TYPE_AIR_QUALITY, COMPLICATION_TYPE_CAST_INFO, COMPLICATION_TYPE_HOME_CONTROLS, - COMPLICATION_TYPE_SMARTSPACE + COMPLICATION_TYPE_SMARTSPACE, + COMPLICATION_TYPE_MEDIA_ENTRY }) @Retention(RetentionPolicy.SOURCE) public @interface ComplicationType { @@ -105,6 +106,7 @@ public class DreamBackend { public static final int COMPLICATION_TYPE_CAST_INFO = 5; public static final int COMPLICATION_TYPE_HOME_CONTROLS = 6; public static final int COMPLICATION_TYPE_SMARTSPACE = 7; + public static final int COMPLICATION_TYPE_MEDIA_ENTRY = 8; private final Context mContext; private final IDreamManager mDreamManager; diff --git a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/DataServiceUtils.java b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/DataServiceUtils.java index 03d9f2db01f2..30d382023b5d 100644 --- a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/DataServiceUtils.java +++ b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/DataServiceUtils.java @@ -357,5 +357,12 @@ public class DataServiceUtils { * {@link SubscriptionManager#getDefaultSubscriptionId()}. */ public static final String COLUMN_IS_DEFAULT_SUBSCRIPTION = "isDefaultSubscription"; + + /** + * The name of the active data subscription state column, see + * {@link SubscriptionManager#getActiveDataSubscriptionId()}. + */ + public static final String COLUMN_IS_ACTIVE_DATA_SUBSCRIPTION = + "isActiveDataSubscriptionId"; } } diff --git a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/MobileNetworkDatabase.java b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/MobileNetworkDatabase.java index c1ee7ad647c6..ca457b04448d 100644 --- a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/MobileNetworkDatabase.java +++ b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/MobileNetworkDatabase.java @@ -20,6 +20,7 @@ import android.content.Context; import android.util.Log; import java.util.List; +import java.util.Objects; import androidx.lifecycle.LiveData; import androidx.room.Database; @@ -39,17 +40,27 @@ public abstract class MobileNetworkDatabase extends RoomDatabase { public abstract MobileNetworkInfoDao mMobileNetworkInfoDao(); + private static MobileNetworkDatabase sInstance; + private static final Object sLOCK = new Object(); + + /** * Create the MobileNetworkDatabase. * * @param context The context. * @return The MobileNetworkDatabase. */ - public static MobileNetworkDatabase createDatabase(Context context) { - return Room.inMemoryDatabaseBuilder(context, MobileNetworkDatabase.class) - .fallbackToDestructiveMigration() - .enableMultiInstanceInvalidation() - .build(); + public static MobileNetworkDatabase getInstance(Context context) { + synchronized (sLOCK) { + if (Objects.isNull(sInstance)) { + Log.d(TAG, "createDatabase."); + sInstance = Room.inMemoryDatabaseBuilder(context, MobileNetworkDatabase.class) + .fallbackToDestructiveMigration() + .enableMultiInstanceInvalidation() + .build(); + } + } + return sInstance; } /** @@ -93,7 +104,7 @@ public abstract class MobileNetworkDatabase extends RoomDatabase { * Query the subscription info by the subscription ID from the SubscriptionInfoEntity * table. */ - public LiveData<SubscriptionInfoEntity> querySubInfoById(String id) { + public SubscriptionInfoEntity querySubInfoById(String id) { return mSubscriptionInfoDao().querySubInfoById(id); } diff --git a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/SubscriptionInfoDao.java b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/SubscriptionInfoDao.java index 45966376ea8a..e835125b9b81 100644 --- a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/SubscriptionInfoDao.java +++ b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/SubscriptionInfoDao.java @@ -37,7 +37,7 @@ public interface SubscriptionInfoDao { @Query("SELECT * FROM " + DataServiceUtils.SubscriptionInfoData.TABLE_NAME + " WHERE " + DataServiceUtils.SubscriptionInfoData.COLUMN_ID + " = :subId") - LiveData<SubscriptionInfoEntity> querySubInfoById(String subId); + SubscriptionInfoEntity querySubInfoById(String subId); @Query("SELECT * FROM " + DataServiceUtils.SubscriptionInfoData.TABLE_NAME + " WHERE " + DataServiceUtils.SubscriptionInfoData.COLUMN_IS_ACTIVE_SUBSCRIPTION_ID diff --git a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/SubscriptionInfoEntity.java b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/SubscriptionInfoEntity.java index 329bd9bfb9e7..23566f760444 100644 --- a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/SubscriptionInfoEntity.java +++ b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/SubscriptionInfoEntity.java @@ -42,7 +42,7 @@ public class SubscriptionInfoEntity { boolean isUsableSubscription, boolean isActiveSubscriptionId, boolean isAvailableSubscription, boolean isDefaultVoiceSubscription, boolean isDefaultSmsSubscription, boolean isDefaultDataSubscription, - boolean isDefaultSubscription) { + boolean isDefaultSubscription, boolean isActiveDataSubscriptionId) { this.subId = subId; this.simSlotIndex = simSlotIndex; this.carrierId = carrierId; @@ -72,6 +72,7 @@ public class SubscriptionInfoEntity { this.isDefaultSmsSubscription = isDefaultSmsSubscription; this.isDefaultDataSubscription = isDefaultDataSubscription; this.isDefaultSubscription = isDefaultSubscription; + this.isActiveDataSubscriptionId = isActiveDataSubscriptionId; } @PrimaryKey @@ -165,6 +166,9 @@ public class SubscriptionInfoEntity { @ColumnInfo(name = DataServiceUtils.SubscriptionInfoData.COLUMN_IS_DEFAULT_SUBSCRIPTION) public boolean isDefaultSubscription; + @ColumnInfo(name = DataServiceUtils.SubscriptionInfoData.COLUMN_IS_ACTIVE_DATA_SUBSCRIPTION) + public boolean isActiveDataSubscriptionId; + public int getSubId() { return Integer.valueOf(subId); } @@ -213,6 +217,7 @@ public class SubscriptionInfoEntity { result = 31 * result + Boolean.hashCode(isDefaultSmsSubscription); result = 31 * result + Boolean.hashCode(isDefaultDataSubscription); result = 31 * result + Boolean.hashCode(isDefaultSubscription); + result = 31 * result + Boolean.hashCode(isActiveDataSubscriptionId); return result; } @@ -254,7 +259,8 @@ public class SubscriptionInfoEntity { && isDefaultVoiceSubscription == info.isDefaultVoiceSubscription && isDefaultSmsSubscription == info.isDefaultSmsSubscription && isDefaultDataSubscription == info.isDefaultDataSubscription - && isDefaultSubscription == info.isDefaultSubscription; + && isDefaultSubscription == info.isDefaultSubscription + && isActiveDataSubscriptionId == info.isActiveDataSubscriptionId; } public String toString() { @@ -317,6 +323,8 @@ public class SubscriptionInfoEntity { .append(isDefaultDataSubscription) .append(", isDefaultSubscription = ") .append(isDefaultSubscription) + .append(", isActiveDataSubscriptionId = ") + .append(isActiveDataSubscriptionId) .append(")}"); return builder.toString(); } diff --git a/packages/SettingsLib/src/com/android/settingslib/qrcode/OWNERS b/packages/SettingsLib/src/com/android/settingslib/qrcode/OWNERS new file mode 100644 index 000000000000..61c73fb733a9 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/qrcode/OWNERS @@ -0,0 +1,8 @@ +# Default reviewers for this and subdirectories. +bonianchen@google.com +changbetty@google.com +goldmanj@google.com +wengsu@google.com +zoeychen@google.com + +# Emergency approvers in case the above are not available diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java index 315ab0aac878..79e99387b2fa 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java @@ -1069,80 +1069,4 @@ public class CachedBluetoothDeviceTest { assertThat(mSubCachedDevice.mDevice).isEqualTo(mDevice); assertThat(mCachedDevice.getMemberDevice().contains(mSubCachedDevice)).isTrue(); } - - @Test - public void isBusy_mainDeviceIsConnecting_returnsBusy() { - mCachedDevice.addMemberDevice(mSubCachedDevice); - updateProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTED); - updateSubDeviceProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTED); - when(mDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED); - when(mSubDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED); - - updateProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTING); - - assertThat(mCachedDevice.getMemberDevice().contains(mSubCachedDevice)).isTrue(); - assertThat(mCachedDevice.getProfiles().contains(mA2dpProfile)).isTrue(); - assertThat(mSubCachedDevice.getProfiles().contains(mA2dpProfile)).isTrue(); - assertThat(mCachedDevice.isBusy()).isTrue(); - } - - @Test - public void isBusy_mainDeviceIsBonding_returnsBusy() { - mCachedDevice.addMemberDevice(mSubCachedDevice); - updateProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTED); - updateSubDeviceProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTED); - when(mSubDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED); - - when(mDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDING); - - assertThat(mCachedDevice.getMemberDevice().contains(mSubCachedDevice)).isTrue(); - assertThat(mCachedDevice.getProfiles().contains(mA2dpProfile)).isTrue(); - assertThat(mSubCachedDevice.getProfiles().contains(mA2dpProfile)).isTrue(); - assertThat(mCachedDevice.isBusy()).isTrue(); - } - - @Test - public void isBusy_memberDeviceIsConnecting_returnsBusy() { - mCachedDevice.addMemberDevice(mSubCachedDevice); - updateProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTED); - updateSubDeviceProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTED); - when(mDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED); - when(mSubDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED); - - updateSubDeviceProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTING); - - assertThat(mCachedDevice.getMemberDevice().contains(mSubCachedDevice)).isTrue(); - assertThat(mCachedDevice.getProfiles().contains(mA2dpProfile)).isTrue(); - assertThat(mSubCachedDevice.getProfiles().contains(mA2dpProfile)).isTrue(); - assertThat(mCachedDevice.isBusy()).isTrue(); - } - - @Test - public void isBusy_memberDeviceIsBonding_returnsBusy() { - mCachedDevice.addMemberDevice(mSubCachedDevice); - updateProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTED); - updateSubDeviceProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTED); - when(mDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED); - - when(mSubDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDING); - - assertThat(mCachedDevice.getMemberDevice().contains(mSubCachedDevice)).isTrue(); - assertThat(mCachedDevice.getProfiles().contains(mA2dpProfile)).isTrue(); - assertThat(mSubCachedDevice.getProfiles().contains(mA2dpProfile)).isTrue(); - assertThat(mCachedDevice.isBusy()).isTrue(); - } - - @Test - public void isBusy_allDevicesAreNotBusy_returnsNotBusy() { - mCachedDevice.addMemberDevice(mSubCachedDevice); - updateProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTED); - updateSubDeviceProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTED); - when(mDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED); - when(mSubDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED); - - assertThat(mCachedDevice.getMemberDevice().contains(mSubCachedDevice)).isTrue(); - assertThat(mCachedDevice.getProfiles().contains(mA2dpProfile)).isTrue(); - assertThat(mSubCachedDevice.getProfiles().contains(mA2dpProfile)).isTrue(); - assertThat(mCachedDevice.isBusy()).isFalse(); - } } diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java index ccbfac226c46..fa96a2f0ee7f 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java @@ -5533,13 +5533,17 @@ public class SettingsProvider extends ContentProvider { } if (currentVersion == 210) { final SettingsState secureSettings = getSecureSettingsLocked(userId); - final int defaultValueVibrateIconEnabled = getContext().getResources() - .getInteger(R.integer.def_statusBarVibrateIconEnabled); - secureSettings.insertSettingOverrideableByRestoreLocked( - Secure.STATUS_BAR_SHOW_VIBRATE_ICON, - String.valueOf(defaultValueVibrateIconEnabled), - null /* tag */, true /* makeDefault */, - SettingsState.SYSTEM_PACKAGE_NAME); + final Setting currentSetting = secureSettings.getSettingLocked( + Secure.STATUS_BAR_SHOW_VIBRATE_ICON); + if (currentSetting.isNull()) { + final int defaultValueVibrateIconEnabled = getContext().getResources() + .getInteger(R.integer.def_statusBarVibrateIconEnabled); + secureSettings.insertSettingOverrideableByRestoreLocked( + Secure.STATUS_BAR_SHOW_VIBRATE_ICON, + String.valueOf(defaultValueVibrateIconEnabled), + null /* tag */, true /* makeDefault */, + SettingsState.SYSTEM_PACKAGE_NAME); + } currentVersion = 211; } // vXXX: Add new settings above this point. diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index b5145f926abd..4267ba2ff0b7 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -289,6 +289,12 @@ <!-- Query all packages on device on R+ --> <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" /> + <queries> + <intent> + <action android:name="android.intent.action.NOTES" /> + </intent> + </queries> + <!-- Permission to register process observer --> <uses-permission android:name="android.permission.SET_ACTIVITY_WATCHER"/> diff --git a/packages/SystemUI/docs/device-entry/quickaffordance.md b/packages/SystemUI/docs/device-entry/quickaffordance.md index 38d636d7ff82..95b986faebb4 100644 --- a/packages/SystemUI/docs/device-entry/quickaffordance.md +++ b/packages/SystemUI/docs/device-entry/quickaffordance.md @@ -8,7 +8,7 @@ credit card, etc. ### Step 1: create a new quick affordance config * Create a new class under the [systemui/keyguard/domain/quickaffordance](../../src/com/android/systemui/keyguard/domain/quickaffordance) directory * Please make sure that the class is injected through the Dagger dependency injection system by using the `@Inject` annotation on its main constructor and the `@SysUISingleton` annotation at class level, to make sure only one instance of the class is ever instantiated -* Have the class implement the [KeyguardQuickAffordanceConfig](../../src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceConfig.kt) interface, notes: +* Have the class implement the [KeyguardQuickAffordanceConfig](../../src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt) interface, notes: * The `state` Flow property must emit `State.Hidden` when the feature is not enabled! * It is safe to assume that `onQuickAffordanceClicked` will not be invoked if-and-only-if the previous rule is followed * When implementing `onQuickAffordanceClicked`, the implementation can do something or it can ask the framework to start an activity using an `Intent` provided by the implementation diff --git a/packages/SystemUI/res/drawable/accessibility_floating_message_background.xml b/packages/SystemUI/res/drawable/accessibility_floating_message_background.xml new file mode 100644 index 000000000000..de83df4e625c --- /dev/null +++ b/packages/SystemUI/res/drawable/accessibility_floating_message_background.xml @@ -0,0 +1,22 @@ +<!-- + ~ Copyright (C) 2022 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<shape xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + android:shape="rectangle"> + <solid android:color="@color/accessibility_floating_menu_message_background"/> + <corners android:radius="28dp"/> +</shape> diff --git a/packages/SystemUI/res/layout-land/auth_credential_password_view.xml b/packages/SystemUI/res/layout-land/auth_credential_password_view.xml index 3bcc37a478c9..e2ce34f5008e 100644 --- a/packages/SystemUI/res/layout-land/auth_credential_password_view.xml +++ b/packages/SystemUI/res/layout-land/auth_credential_password_view.xml @@ -14,7 +14,7 @@ ~ limitations under the License. --> -<com.android.systemui.biometrics.AuthCredentialPasswordView +<com.android.systemui.biometrics.ui.CredentialPasswordView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" @@ -86,4 +86,4 @@ </LinearLayout> -</com.android.systemui.biometrics.AuthCredentialPasswordView>
\ No newline at end of file +</com.android.systemui.biometrics.ui.CredentialPasswordView>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml b/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml index a3dd334bd667..6e0e38b95ee5 100644 --- a/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml +++ b/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml @@ -14,7 +14,7 @@ ~ limitations under the License. --> -<com.android.systemui.biometrics.AuthCredentialPatternView +<com.android.systemui.biometrics.ui.CredentialPatternView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" @@ -83,4 +83,4 @@ </FrameLayout> -</com.android.systemui.biometrics.AuthCredentialPatternView>
\ No newline at end of file +</com.android.systemui.biometrics.ui.CredentialPatternView>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/auth_credential_password_view.xml b/packages/SystemUI/res/layout/auth_credential_password_view.xml index 774b335f913e..021ebe6e7bff 100644 --- a/packages/SystemUI/res/layout/auth_credential_password_view.xml +++ b/packages/SystemUI/res/layout/auth_credential_password_view.xml @@ -14,7 +14,7 @@ ~ limitations under the License. --> -<com.android.systemui.biometrics.AuthCredentialPasswordView +<com.android.systemui.biometrics.ui.CredentialPasswordView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" @@ -83,4 +83,4 @@ </LinearLayout> -</com.android.systemui.biometrics.AuthCredentialPasswordView>
\ No newline at end of file +</com.android.systemui.biometrics.ui.CredentialPasswordView>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/auth_credential_pattern_view.xml b/packages/SystemUI/res/layout/auth_credential_pattern_view.xml index 4af997017bba..891c6af4b667 100644 --- a/packages/SystemUI/res/layout/auth_credential_pattern_view.xml +++ b/packages/SystemUI/res/layout/auth_credential_pattern_view.xml @@ -14,7 +14,7 @@ ~ limitations under the License. --> -<com.android.systemui.biometrics.AuthCredentialPatternView +<com.android.systemui.biometrics.ui.CredentialPatternView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" @@ -78,4 +78,4 @@ android:layout_gravity="center_horizontal|bottom"/> </FrameLayout> -</com.android.systemui.biometrics.AuthCredentialPatternView>
\ No newline at end of file +</com.android.systemui.biometrics.ui.CredentialPatternView> diff --git a/packages/SystemUI/res/values-night/colors.xml b/packages/SystemUI/res/values-night/colors.xml index dc2bee56373c..16152f80308a 100644 --- a/packages/SystemUI/res/values-night/colors.xml +++ b/packages/SystemUI/res/values-night/colors.xml @@ -99,6 +99,8 @@ <!-- Accessibility floating menu --> <color name="accessibility_floating_menu_background">#B3000000</color> <!-- 70% --> + <color name="accessibility_floating_menu_message_background">@*android:color/background_material_dark</color> + <color name="accessibility_floating_menu_message_text">@*android:color/primary_text_default_material_dark</color> <color name="people_tile_background">@color/material_dynamic_secondary20</color> </resources> diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml index 9e8bef06270b..55b59b63c2f9 100644 --- a/packages/SystemUI/res/values/colors.xml +++ b/packages/SystemUI/res/values/colors.xml @@ -219,6 +219,8 @@ <!-- Accessibility floating menu --> <color name="accessibility_floating_menu_background">#CCFFFFFF</color> <!-- 80% --> <color name="accessibility_floating_menu_stroke_dark">#26FFFFFF</color> <!-- 15% --> + <color name="accessibility_floating_menu_message_background">@*android:color/background_material_light</color> + <color name="accessibility_floating_menu_message_text">@*android:color/primary_text_default_material_light</color> <!-- Wallet screen --> <color name="wallet_card_border">#33FFFFFF</color> diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 9188ce091a3b..93982cb2c5b9 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -643,6 +643,18 @@ <item>26</item> <!-- MOUTH_COVERING_DETECTED --> </integer-array> + <!-- Which device wake-ups will trigger face auth. These values correspond with + PowerManager#WakeReason. --> + <integer-array name="config_face_auth_wake_up_triggers"> + <item>1</item> <!-- WAKE_REASON_POWER_BUTTON --> + <item>4</item> <!-- WAKE_REASON_GESTURE --> + <item>6</item> <!-- WAKE_REASON_WAKE_KEY --> + <item>7</item> <!-- WAKE_REASON_WAKE_MOTION --> + <item>9</item> <!-- WAKE_REASON_LID --> + <item>10</item> <!-- WAKE_REASON_DISPLAY_GROUP_ADDED --> + <item>12</item> <!-- WAKE_REASON_UNFOLD_DEVICE --> + </integer-array> + <!-- Whether the communal service should be enabled --> <bool name="config_communalServiceEnabled">false</bool> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 66f0e7543469..f02f29a4f741 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1336,6 +1336,14 @@ <dimen name="accessibility_floating_menu_large_single_radius">35dp</dimen> <dimen name="accessibility_floating_menu_large_multiple_radius">35dp</dimen> + <dimen name="accessibility_floating_menu_message_container_horizontal_padding">15dp</dimen> + <dimen name="accessibility_floating_menu_message_text_vertical_padding">8dp</dimen> + <dimen name="accessibility_floating_menu_message_margin">8dp</dimen> + <dimen name="accessibility_floating_menu_message_elevation">5dp</dimen> + <dimen name="accessibility_floating_menu_message_text_size">14sp</dimen> + <dimen name="accessibility_floating_menu_message_min_width">312dp</dimen> + <dimen name="accessibility_floating_menu_message_min_height">48dp</dimen> + <dimen name="accessibility_floating_tooltip_arrow_width">8dp</dimen> <dimen name="accessibility_floating_tooltip_arrow_height">16dp</dimen> <dimen name="accessibility_floating_tooltip_arrow_margin">-2dp</dimen> diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml index 7ca42f7d7015..4fd25a98a71c 100644 --- a/packages/SystemUI/res/values/ids.xml +++ b/packages/SystemUI/res/values/ids.xml @@ -177,6 +177,7 @@ <item type="id" name="action_move_bottom_right"/> <item type="id" name="action_move_to_edge_and_hide"/> <item type="id" name="action_move_out_edge_and_show"/> + <item type="id" name="action_remove_menu"/> <!-- rounded corner view id --> <item type="id" name="rounded_corner_top_left"/> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 44031bb99751..b325c56adefc 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2197,6 +2197,15 @@ <string name="accessibility_floating_button_migration_tooltip">Tap to open accessibility features. Customize or replace this button in Settings.\n\n<annotation id="link">View settings</annotation></string> <!-- Message for the accessibility floating button docking tooltip. It shows when the user first time drag the button. It will tell the user about docking behavior. [CHAR LIMIT=70] --> <string name="accessibility_floating_button_docking_tooltip">Move button to the edge to hide it temporarily</string> + <!-- Text for the undo action button of the message view of the accessibility floating menu to perform undo operation. [CHAR LIMIT=30]--> + <string name="accessibility_floating_button_undo">Undo</string> + + <!-- Text for the message view with undo action of the accessibility floating menu to show how many features shortcuts were removed. [CHAR LIMIT=30]--> + <string name="accessibility_floating_button_undo_message_text">{count, plural, + =1 {{label} shortcut removed} + other {# shortcuts removed} + }</string> + <!-- Action in accessibility menu to move the accessibility floating button to the top left of the screen. [CHAR LIMIT=30] --> <string name="accessibility_floating_button_action_move_top_left">Move top left</string> <!-- Action in accessibility menu to move the accessibility floating button to the top right of the screen. [CHAR LIMIT=30] --> @@ -2209,6 +2218,8 @@ <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half">Move to edge and hide</string> <!-- Action in accessibility menu to move the accessibility floating button out the edge and show. [CHAR LIMIT=36]--> <string name="accessibility_floating_button_action_move_out_edge_and_show">Move out edge and show</string> + <!-- Action in accessibility menu to remove the accessibility floating menu view on the screen. [CHAR LIMIT=36]--> + <string name="accessibility_floating_button_action_remove_menu">Remove</string> <!-- Action in accessibility menu to toggle on/off the accessibility feature. [CHAR LIMIT=30]--> <string name="accessibility_floating_button_action_double_tap_to_toggle">toggle</string> diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/pip/PipSurfaceTransactionHelper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/pip/PipSurfaceTransactionHelper.java index 7e42e1b89b1f..8ac1de898be8 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/pip/PipSurfaceTransactionHelper.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/pip/PipSurfaceTransactionHelper.java @@ -85,13 +85,12 @@ public class PipSurfaceTransactionHelper { mTmpSourceRectF.set(sourceBounds); mTmpDestinationRect.set(sourceBounds); mTmpDestinationRect.inset(insets); - // Scale by the shortest edge and offset such that the top/left of the scaled inset - // source rect aligns with the top/left of the destination bounds + // Scale to the bounds no smaller than the destination and offset such that the top/left + // of the scaled inset source rect aligns with the top/left of the destination bounds final float scale; if (sourceRectHint.isEmpty() || sourceRectHint.width() == sourceBounds.width()) { - scale = sourceBounds.width() <= sourceBounds.height() - ? (float) destinationBounds.width() / sourceBounds.width() - : (float) destinationBounds.height() / sourceBounds.height(); + scale = Math.max((float) destinationBounds.width() / sourceBounds.width(), + (float) destinationBounds.height() / sourceBounds.height()); } else { // scale by sourceRectHint if it's not edge-to-edge final float endScale = sourceRectHint.width() <= sourceRectHint.height() diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java index 8d1768c41589..e1e806319ba0 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java @@ -26,6 +26,7 @@ import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.TRANSIT_TO_FRONT; +import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY; import static android.window.TransitionInfo.FLAG_IS_WALLPAPER; import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT; @@ -228,7 +229,8 @@ public class RemoteAnimationTargetCompat { public static RemoteAnimationTarget[] wrapNonApps(TransitionInfo info, boolean wallpapers, SurfaceControl.Transaction t, ArrayMap<SurfaceControl, SurfaceControl> leashMap) { return wrap(info, t, leashMap, (change, taskInfo) -> (taskInfo == null) - && wallpapers == ((change.getFlags() & TransitionInfo.FLAG_IS_WALLPAPER) != 0)); + && wallpapers == change.hasFlags(FLAG_IS_WALLPAPER) + && !change.hasFlags(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY)); } private static RemoteAnimationTarget[] wrap(TransitionInfo info, diff --git a/packages/SystemUI/src-debug/com/android/systemui/flags/FlagsModule.kt b/packages/SystemUI/src-debug/com/android/systemui/flags/FlagsModule.kt index bb3df8f0358a..7b216017df7d 100644 --- a/packages/SystemUI/src-debug/com/android/systemui/flags/FlagsModule.kt +++ b/packages/SystemUI/src-debug/com/android/systemui/flags/FlagsModule.kt @@ -18,17 +18,15 @@ package com.android.systemui.flags import android.content.Context import android.os.Handler -import com.android.internal.statusbar.IStatusBarService import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.flags.FeatureFlagsDebug.ALL_FLAGS import com.android.systemui.util.settings.SettingsUtilModule import dagger.Binds import dagger.Module import dagger.Provides -import javax.inject.Named @Module(includes = [ FeatureFlagsDebugStartableModule::class, + FlagsCommonModule::class, ServerFlagReaderModule::class, SettingsUtilModule::class, ]) @@ -43,20 +41,5 @@ abstract class FlagsModule { fun provideFlagManager(context: Context, @Main handler: Handler): FlagManager { return FlagManager(context, handler) } - - @JvmStatic - @Provides - @Named(ALL_FLAGS) - fun providesAllFlags(): Map<Int, Flag<*>> = Flags.collectFlags() - - @JvmStatic - @Provides - fun providesRestarter(barService: IStatusBarService): Restarter { - return object: Restarter { - override fun restart() { - barService.restart() - } - } - } } } diff --git a/packages/SystemUI/src-release/com/android/systemui/flags/FlagsModule.kt b/packages/SystemUI/src-release/com/android/systemui/flags/FlagsModule.kt index 0f7e732fceb1..aef887667527 100644 --- a/packages/SystemUI/src-release/com/android/systemui/flags/FlagsModule.kt +++ b/packages/SystemUI/src-release/com/android/systemui/flags/FlagsModule.kt @@ -16,29 +16,15 @@ package com.android.systemui.flags -import com.android.internal.statusbar.IStatusBarService import dagger.Binds import dagger.Module -import dagger.Provides @Module(includes = [ FeatureFlagsReleaseStartableModule::class, + FlagsCommonModule::class, ServerFlagReaderModule::class ]) abstract class FlagsModule { @Binds abstract fun bindsFeatureFlagRelease(impl: FeatureFlagsRelease): FeatureFlags - - @Module - companion object { - @JvmStatic - @Provides - fun providesRestarter(barService: IStatusBarService): Restarter { - return object: Restarter { - override fun restart() { - barService.restart() - } - } - } - } } diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt index 386c09575a1a..40a96b060bc0 100644 --- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt +++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt @@ -30,6 +30,7 @@ import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags.DOZING_MIGRATION_1 import com.android.systemui.flags.Flags.REGION_SAMPLING import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor @@ -221,8 +222,11 @@ open class ClockEventController @Inject constructor( disposableHandle = parent.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.STARTED) { listenForDozing(this) - listenForDozeAmount(this) - listenForDozeAmountTransition(this) + if (featureFlags.isEnabled(DOZING_MIGRATION_1)) { + listenForDozeAmountTransition(this) + } else { + listenForDozeAmount(this) + } } } } @@ -265,10 +269,9 @@ open class ClockEventController @Inject constructor( @VisibleForTesting internal fun listenForDozeAmountTransition(scope: CoroutineScope): Job { return scope.launch { - keyguardTransitionInteractor.aodToLockscreenTransition.collect { - // Would eventually run this: - // dozeAmount = it.value - // clock?.animations?.doze(dozeAmount) + keyguardTransitionInteractor.dozeAmountTransition.collect { + dozeAmount = it.value + clock?.animations?.doze(dozeAmount) } } } diff --git a/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt b/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt index 6fcb6f55dc20..4a41b3fe2589 100644 --- a/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt +++ b/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt @@ -17,6 +17,7 @@ package com.android.keyguard import android.annotation.StringDef +import android.os.PowerManager import com.android.internal.logging.UiEvent import com.android.internal.logging.UiEventLogger import com.android.keyguard.FaceAuthApiRequestReason.Companion.NOTIFICATION_PANEL_CLICKED @@ -122,122 +123,93 @@ private object InternalFaceAuthReasons { "Face auth started/stopped because biometric is enabled on keyguard" } -/** UiEvents that are logged to identify why face auth is being triggered. */ -enum class FaceAuthUiEvent constructor(private val id: Int, val reason: String) : +/** + * UiEvents that are logged to identify why face auth is being triggered. + * @param extraInfo is logged as the position. See [UiEventLogger#logWithInstanceIdAndPosition] + */ +enum class FaceAuthUiEvent +constructor(private val id: Int, val reason: String, var extraInfo: Int = 0) : UiEventLogger.UiEventEnum { @UiEvent(doc = OCCLUDING_APP_REQUESTED) FACE_AUTH_TRIGGERED_OCCLUDING_APP_REQUESTED(1146, OCCLUDING_APP_REQUESTED), - @UiEvent(doc = UDFPS_POINTER_DOWN) FACE_AUTH_TRIGGERED_UDFPS_POINTER_DOWN(1147, UDFPS_POINTER_DOWN), - @UiEvent(doc = SWIPE_UP_ON_BOUNCER) FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER(1148, SWIPE_UP_ON_BOUNCER), - @UiEvent(doc = DEVICE_WOKEN_UP_ON_REACH_GESTURE) FACE_AUTH_TRIGGERED_ON_REACH_GESTURE_ON_AOD(1149, DEVICE_WOKEN_UP_ON_REACH_GESTURE), - @UiEvent(doc = FACE_LOCKOUT_RESET) FACE_AUTH_TRIGGERED_FACE_LOCKOUT_RESET(1150, FACE_LOCKOUT_RESET), - - @UiEvent(doc = QS_EXPANDED) - FACE_AUTH_TRIGGERED_QS_EXPANDED(1151, QS_EXPANDED), - + @UiEvent(doc = QS_EXPANDED) FACE_AUTH_TRIGGERED_QS_EXPANDED(1151, QS_EXPANDED), @UiEvent(doc = NOTIFICATION_PANEL_CLICKED) FACE_AUTH_TRIGGERED_NOTIFICATION_PANEL_CLICKED(1152, NOTIFICATION_PANEL_CLICKED), - @UiEvent(doc = PICK_UP_GESTURE_TRIGGERED) FACE_AUTH_TRIGGERED_PICK_UP_GESTURE_TRIGGERED(1153, PICK_UP_GESTURE_TRIGGERED), - @UiEvent(doc = ALTERNATE_BIOMETRIC_BOUNCER_SHOWN) - FACE_AUTH_TRIGGERED_ALTERNATE_BIOMETRIC_BOUNCER_SHOWN(1154, - ALTERNATE_BIOMETRIC_BOUNCER_SHOWN), - + FACE_AUTH_TRIGGERED_ALTERNATE_BIOMETRIC_BOUNCER_SHOWN(1154, ALTERNATE_BIOMETRIC_BOUNCER_SHOWN), @UiEvent(doc = PRIMARY_BOUNCER_SHOWN) FACE_AUTH_UPDATED_PRIMARY_BOUNCER_SHOWN(1155, PRIMARY_BOUNCER_SHOWN), - @UiEvent(doc = PRIMARY_BOUNCER_SHOWN_OR_WILL_BE_SHOWN) FACE_AUTH_UPDATED_PRIMARY_BOUNCER_SHOWN_OR_WILL_BE_SHOWN( 1197, PRIMARY_BOUNCER_SHOWN_OR_WILL_BE_SHOWN ), - @UiEvent(doc = RETRY_AFTER_HW_UNAVAILABLE) FACE_AUTH_TRIGGERED_RETRY_AFTER_HW_UNAVAILABLE(1156, RETRY_AFTER_HW_UNAVAILABLE), - - @UiEvent(doc = TRUST_DISABLED) - FACE_AUTH_TRIGGERED_TRUST_DISABLED(1158, TRUST_DISABLED), - - @UiEvent(doc = TRUST_ENABLED) - FACE_AUTH_STOPPED_TRUST_ENABLED(1173, TRUST_ENABLED), - + @UiEvent(doc = TRUST_DISABLED) FACE_AUTH_TRIGGERED_TRUST_DISABLED(1158, TRUST_DISABLED), + @UiEvent(doc = TRUST_ENABLED) FACE_AUTH_STOPPED_TRUST_ENABLED(1173, TRUST_ENABLED), @UiEvent(doc = KEYGUARD_OCCLUSION_CHANGED) FACE_AUTH_UPDATED_KEYGUARD_OCCLUSION_CHANGED(1159, KEYGUARD_OCCLUSION_CHANGED), - @UiEvent(doc = ASSISTANT_VISIBILITY_CHANGED) FACE_AUTH_UPDATED_ASSISTANT_VISIBILITY_CHANGED(1160, ASSISTANT_VISIBILITY_CHANGED), - @UiEvent(doc = STARTED_WAKING_UP) - FACE_AUTH_UPDATED_STARTED_WAKING_UP(1161, STARTED_WAKING_UP), - + FACE_AUTH_UPDATED_STARTED_WAKING_UP(1161, STARTED_WAKING_UP) { + override fun extraInfoToString(): String { + return PowerManager.wakeReasonToString(extraInfo) + } + }, + @Deprecated( + "Not a face auth trigger.", + ReplaceWith( + "FACE_AUTH_UPDATED_STARTED_WAKING_UP, " + + "extraInfo=PowerManager.WAKE_REASON_DREAM_FINISHED" + ) + ) @UiEvent(doc = DREAM_STOPPED) FACE_AUTH_TRIGGERED_DREAM_STOPPED(1162, DREAM_STOPPED), - @UiEvent(doc = ALL_AUTHENTICATORS_REGISTERED) FACE_AUTH_TRIGGERED_ALL_AUTHENTICATORS_REGISTERED(1163, ALL_AUTHENTICATORS_REGISTERED), - @UiEvent(doc = ENROLLMENTS_CHANGED) FACE_AUTH_TRIGGERED_ENROLLMENTS_CHANGED(1164, ENROLLMENTS_CHANGED), - @UiEvent(doc = KEYGUARD_VISIBILITY_CHANGED) FACE_AUTH_UPDATED_KEYGUARD_VISIBILITY_CHANGED(1165, KEYGUARD_VISIBILITY_CHANGED), - @UiEvent(doc = FACE_CANCEL_NOT_RECEIVED) FACE_AUTH_STOPPED_FACE_CANCEL_NOT_RECEIVED(1174, FACE_CANCEL_NOT_RECEIVED), - @UiEvent(doc = AUTH_REQUEST_DURING_CANCELLATION) FACE_AUTH_TRIGGERED_DURING_CANCELLATION(1175, AUTH_REQUEST_DURING_CANCELLATION), - - @UiEvent(doc = DREAM_STARTED) - FACE_AUTH_STOPPED_DREAM_STARTED(1176, DREAM_STARTED), - - @UiEvent(doc = FP_LOCKED_OUT) - FACE_AUTH_STOPPED_FP_LOCKED_OUT(1177, FP_LOCKED_OUT), - + @UiEvent(doc = DREAM_STARTED) FACE_AUTH_STOPPED_DREAM_STARTED(1176, DREAM_STARTED), + @UiEvent(doc = FP_LOCKED_OUT) FACE_AUTH_STOPPED_FP_LOCKED_OUT(1177, FP_LOCKED_OUT), @UiEvent(doc = FACE_AUTH_STOPPED_ON_USER_INPUT) FACE_AUTH_STOPPED_USER_INPUT_ON_BOUNCER(1178, FACE_AUTH_STOPPED_ON_USER_INPUT), - @UiEvent(doc = KEYGUARD_GOING_AWAY) FACE_AUTH_STOPPED_KEYGUARD_GOING_AWAY(1179, KEYGUARD_GOING_AWAY), - - @UiEvent(doc = CAMERA_LAUNCHED) - FACE_AUTH_UPDATED_CAMERA_LAUNCHED(1180, CAMERA_LAUNCHED), - - @UiEvent(doc = FP_AUTHENTICATED) - FACE_AUTH_UPDATED_FP_AUTHENTICATED(1181, FP_AUTHENTICATED), - - @UiEvent(doc = GOING_TO_SLEEP) - FACE_AUTH_UPDATED_GOING_TO_SLEEP(1182, GOING_TO_SLEEP), - + @UiEvent(doc = CAMERA_LAUNCHED) FACE_AUTH_UPDATED_CAMERA_LAUNCHED(1180, CAMERA_LAUNCHED), + @UiEvent(doc = FP_AUTHENTICATED) FACE_AUTH_UPDATED_FP_AUTHENTICATED(1181, FP_AUTHENTICATED), + @UiEvent(doc = GOING_TO_SLEEP) FACE_AUTH_UPDATED_GOING_TO_SLEEP(1182, GOING_TO_SLEEP), @UiEvent(doc = FINISHED_GOING_TO_SLEEP) FACE_AUTH_STOPPED_FINISHED_GOING_TO_SLEEP(1183, FINISHED_GOING_TO_SLEEP), - - @UiEvent(doc = KEYGUARD_INIT) - FACE_AUTH_UPDATED_ON_KEYGUARD_INIT(1189, KEYGUARD_INIT), - - @UiEvent(doc = KEYGUARD_RESET) - FACE_AUTH_UPDATED_KEYGUARD_RESET(1185, KEYGUARD_RESET), - - @UiEvent(doc = USER_SWITCHING) - FACE_AUTH_UPDATED_USER_SWITCHING(1186, USER_SWITCHING), - + @UiEvent(doc = KEYGUARD_INIT) FACE_AUTH_UPDATED_ON_KEYGUARD_INIT(1189, KEYGUARD_INIT), + @UiEvent(doc = KEYGUARD_RESET) FACE_AUTH_UPDATED_KEYGUARD_RESET(1185, KEYGUARD_RESET), + @UiEvent(doc = USER_SWITCHING) FACE_AUTH_UPDATED_USER_SWITCHING(1186, USER_SWITCHING), @UiEvent(doc = FACE_AUTHENTICATED) FACE_AUTH_UPDATED_ON_FACE_AUTHENTICATED(1187, FACE_AUTHENTICATED), - @UiEvent(doc = BIOMETRIC_ENABLED) FACE_AUTH_UPDATED_BIOMETRIC_ENABLED_ON_KEYGUARD(1188, BIOMETRIC_ENABLED); override fun getId(): Int = this.id + + /** Convert [extraInfo] to a human-readable string. By default, this is empty. */ + open fun extraInfoToString(): String = "" } private val apiRequestReasonToUiEvent = diff --git a/packages/SystemUI/src/com/android/keyguard/FaceWakeUpTriggersConfig.kt b/packages/SystemUI/src/com/android/keyguard/FaceWakeUpTriggersConfig.kt new file mode 100644 index 000000000000..a0c43fba4bc1 --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/FaceWakeUpTriggersConfig.kt @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.keyguard + +import android.content.res.Resources +import android.os.Build +import android.os.PowerManager +import com.android.systemui.Dumpable +import com.android.systemui.R +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.dump.DumpManager +import com.android.systemui.util.settings.GlobalSettings +import java.io.PrintWriter +import java.util.stream.Collectors +import javax.inject.Inject + +/** Determines which device wake-ups should trigger face authentication. */ +@SysUISingleton +class FaceWakeUpTriggersConfig +@Inject +constructor(@Main resources: Resources, globalSettings: GlobalSettings, dumpManager: DumpManager) : + Dumpable { + private val defaultTriggerFaceAuthOnWakeUpFrom: Set<Int> = + resources.getIntArray(R.array.config_face_auth_wake_up_triggers).toSet() + private val triggerFaceAuthOnWakeUpFrom: Set<Int> + + init { + triggerFaceAuthOnWakeUpFrom = + if (Build.IS_DEBUGGABLE) { + // Update face wake triggers via adb on debuggable builds: + // ie: adb shell settings put global face_wake_triggers "1\|4" && + // adb shell am crash com.android.systemui + processStringArray( + globalSettings.getString("face_wake_triggers"), + defaultTriggerFaceAuthOnWakeUpFrom + ) + } else { + defaultTriggerFaceAuthOnWakeUpFrom + } + dumpManager.registerDumpable(this) + } + + fun shouldTriggerFaceAuthOnWakeUpFrom(@PowerManager.WakeReason pmWakeReason: Int): Boolean { + return triggerFaceAuthOnWakeUpFrom.contains(pmWakeReason) + } + + override fun dump(pw: PrintWriter, args: Array<out String>) { + pw.println("FaceWakeUpTriggers:") + for (pmWakeReason in triggerFaceAuthOnWakeUpFrom) { + pw.println(" ${PowerManager.wakeReasonToString(pmWakeReason)}") + } + } + + /** Convert a pipe-separated set of integers into a set of ints. */ + private fun processStringArray(stringSetting: String?, default: Set<Int>): Set<Int> { + return stringSetting?.let { + stringSetting.split("|").stream().map(Integer::parseInt).collect(Collectors.toSet()) + } + ?: default + } +} diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java index 81305f90e2b8..0b395a8760cf 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java @@ -223,7 +223,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard @Override public void onSwipeUp() { if (!mUpdateMonitor.isFaceDetectionRunning()) { - boolean didFaceAuthRun = mUpdateMonitor.requestFaceAuth(true, + boolean didFaceAuthRun = mUpdateMonitor.requestFaceAuth( FaceAuthApiRequestReason.SWIPE_UP_ON_BOUNCER); mKeyguardSecurityCallback.userActivity(); if (didFaceAuthRun) { diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index aff9dcbc26e3..39dc609c9bb7 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -44,7 +44,6 @@ import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_STOPPED_TRUST_ENABL import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_STOPPED_USER_INPUT_ON_BOUNCER; import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_TRIGGERED_ALL_AUTHENTICATORS_REGISTERED; import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_TRIGGERED_ALTERNATE_BIOMETRIC_BOUNCER_SHOWN; -import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_TRIGGERED_DREAM_STOPPED; import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_TRIGGERED_DURING_CANCELLATION; import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_TRIGGERED_ENROLLMENTS_CHANGED; import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_TRIGGERED_FACE_LOCKOUT_RESET; @@ -287,6 +286,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } } }; + private final FaceWakeUpTriggersConfig mFaceWakeUpTriggersConfig; HashMap<Integer, SimData> mSimDatas = new HashMap<>(); HashMap<Integer, ServiceState> mServiceStates = new HashMap<>(); @@ -1615,7 +1615,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab @Override public void onUdfpsPointerDown(int sensorId) { mLogger.logUdfpsPointerDown(sensorId); - requestFaceAuth(true, FaceAuthApiRequestReason.UDFPS_POINTER_DOWN); + requestFaceAuth(FaceAuthApiRequestReason.UDFPS_POINTER_DOWN); } /** @@ -1807,11 +1807,21 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } } - protected void handleStartedWakingUp() { + protected void handleStartedWakingUp(@PowerManager.WakeReason int pmWakeReason) { Trace.beginSection("KeyguardUpdateMonitor#handleStartedWakingUp"); Assert.isMainThread(); - updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE, FACE_AUTH_UPDATED_STARTED_WAKING_UP); - requestActiveUnlock(ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.WAKE, "wakingUp"); + + updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE); + if (mFaceWakeUpTriggersConfig.shouldTriggerFaceAuthOnWakeUpFrom(pmWakeReason)) { + FACE_AUTH_UPDATED_STARTED_WAKING_UP.setExtraInfo(pmWakeReason); + updateFaceListeningState(BIOMETRIC_ACTION_UPDATE, + FACE_AUTH_UPDATED_STARTED_WAKING_UP); + requestActiveUnlock(ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.WAKE, "wakingUp - " + + PowerManager.wakeReasonToString(pmWakeReason)); + } else { + mLogger.logSkipUpdateFaceListeningOnWakeup(pmWakeReason); + } + for (int i = 0; i < mCallbacks.size(); i++) { KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); if (cb != null) { @@ -1863,12 +1873,9 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab cb.onDreamingStateChanged(mIsDreaming); } } + updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE); if (mIsDreaming) { - updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE); updateFaceListeningState(BIOMETRIC_ACTION_STOP, FACE_AUTH_STOPPED_DREAM_STARTED); - } else { - updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE, - FACE_AUTH_TRIGGERED_DREAM_STOPPED); } } @@ -1948,7 +1955,8 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab PackageManager packageManager, @Nullable FaceManager faceManager, @Nullable FingerprintManager fingerprintManager, - @Nullable BiometricManager biometricManager) { + @Nullable BiometricManager biometricManager, + FaceWakeUpTriggersConfig faceWakeUpTriggersConfig) { mContext = context; mSubscriptionManager = subscriptionManager; mTelephonyListenerManager = telephonyListenerManager; @@ -1987,6 +1995,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab R.array.config_face_acquire_device_entry_ignorelist)) .boxed() .collect(Collectors.toSet()); + mFaceWakeUpTriggersConfig = faceWakeUpTriggersConfig; mHandler = new Handler(mainLooper) { @Override @@ -2036,7 +2045,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab break; case MSG_STARTED_WAKING_UP: Trace.beginSection("KeyguardUpdateMonitor#handler MSG_STARTED_WAKING_UP"); - handleStartedWakingUp(); + handleStartedWakingUp(msg.arg1); Trace.endSection(); break; case MSG_SIM_SUBSCRIPTION_INFO_CHANGED: @@ -2227,8 +2236,8 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab private void updateFaceEnrolled(int userId) { mIsFaceEnrolled = whitelistIpcs( () -> mFaceManager != null && mFaceManager.isHardwareDetected() - && mFaceManager.hasEnrolledTemplates(userId) - && mBiometricEnabledForUser.get(userId)); + && mBiometricEnabledForUser.get(userId)) + && mAuthController.isFaceAuthEnrolled(userId); } public boolean isFaceSupported() { @@ -2348,14 +2357,12 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab /** * Requests face authentication if we're on a state where it's allowed. * This will re-trigger auth in case it fails. - * @param userInitiatedRequest true if the user explicitly requested face auth * @param reason One of the reasons {@link FaceAuthApiRequestReason} on why this API is being * invoked. * @return current face auth detection state, true if it is running. */ - public boolean requestFaceAuth(boolean userInitiatedRequest, - @FaceAuthApiRequestReason String reason) { - mLogger.logFaceAuthRequested(userInitiatedRequest, reason); + public boolean requestFaceAuth(@FaceAuthApiRequestReason String reason) { + mLogger.logFaceAuthRequested(reason); updateFaceListeningState(BIOMETRIC_ACTION_START, apiRequestReasonToUiEvent(reason)); return isFaceDetectionRunning(); } @@ -2784,8 +2791,14 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab // Waiting for ERROR_CANCELED before requesting auth again return; } - mLogger.logStartedListeningForFace(mFaceRunningState, faceAuthUiEvent.getReason()); - mUiEventLogger.log(faceAuthUiEvent, getKeyguardSessionId()); + mLogger.logStartedListeningForFace(mFaceRunningState, faceAuthUiEvent); + mUiEventLogger.logWithInstanceIdAndPosition( + faceAuthUiEvent, + 0, + null, + getKeyguardSessionId(), + faceAuthUiEvent.getExtraInfo() + ); if (unlockPossible) { mFaceCancelSignal = new CancellationSignal(); @@ -3564,11 +3577,16 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab // TODO: use these callbacks elsewhere in place of the existing notifyScreen*() // (KeyguardViewMediator, KeyguardHostView) - public void dispatchStartedWakingUp() { + /** + * Dispatch wakeup events to: + * - update biometric listening states + * - send to registered KeyguardUpdateMonitorCallbacks + */ + public void dispatchStartedWakingUp(@PowerManager.WakeReason int pmWakeReason) { synchronized (this) { mDeviceInteractive = true; } - mHandler.sendEmptyMessage(MSG_STARTED_WAKING_UP); + mHandler.sendMessage(mHandler.obtainMessage(MSG_STARTED_WAKING_UP, pmWakeReason, 0)); } public void dispatchStartedGoingToSleep(int why) { diff --git a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java index 70758dfec932..8fbbd3840964 100644 --- a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java @@ -24,6 +24,8 @@ import static com.android.keyguard.LockIconView.ICON_LOCK; import static com.android.keyguard.LockIconView.ICON_UNLOCK; import static com.android.systemui.classifier.Classifier.LOCK_ICON; import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInOffset; +import static com.android.systemui.flags.Flags.DOZING_MIGRATION_1; +import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow; import android.content.res.Configuration; import android.content.res.Resources; @@ -46,6 +48,7 @@ import android.view.accessibility.AccessibilityNodeInfo; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; import com.android.systemui.Dumpable; @@ -55,6 +58,10 @@ import com.android.systemui.biometrics.AuthRippleController; import com.android.systemui.biometrics.UdfpsController; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; +import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; +import com.android.systemui.keyguard.shared.model.TransitionStep; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.StatusBarState; @@ -67,6 +74,7 @@ import com.android.systemui.util.concurrency.DelayableExecutor; import java.io.PrintWriter; import java.util.Objects; +import java.util.function.Consumer; import javax.inject.Inject; @@ -103,6 +111,9 @@ public class LockIconViewController extends ViewController<LockIconView> impleme @NonNull private CharSequence mLockedLabel; @NonNull private final VibratorHelper mVibrator; @Nullable private final AuthRippleController mAuthRippleController; + @NonNull private final FeatureFlags mFeatureFlags; + @NonNull private final KeyguardTransitionInteractor mTransitionInteractor; + @NonNull private final KeyguardInteractor mKeyguardInteractor; // Tracks the velocity of a touch to help filter out the touches that move too fast. private VelocityTracker mVelocityTracker; @@ -139,6 +150,20 @@ public class LockIconViewController extends ViewController<LockIconView> impleme private boolean mDownDetected; private final Rect mSensorTouchLocation = new Rect(); + @VisibleForTesting + final Consumer<TransitionStep> mDozeTransitionCallback = (TransitionStep step) -> { + mInterpolatedDarkAmount = step.getValue(); + mView.setDozeAmount(step.getValue()); + updateBurnInOffsets(); + }; + + @VisibleForTesting + final Consumer<Boolean> mIsDozingCallback = (Boolean isDozing) -> { + mIsDozing = isDozing; + updateBurnInOffsets(); + updateVisibility(); + }; + @Inject public LockIconViewController( @Nullable LockIconView view, @@ -154,7 +179,10 @@ public class LockIconViewController extends ViewController<LockIconView> impleme @NonNull @Main DelayableExecutor executor, @NonNull VibratorHelper vibrator, @Nullable AuthRippleController authRippleController, - @NonNull @Main Resources resources + @NonNull @Main Resources resources, + @NonNull KeyguardTransitionInteractor transitionInteractor, + @NonNull KeyguardInteractor keyguardInteractor, + @NonNull FeatureFlags featureFlags ) { super(view); mStatusBarStateController = statusBarStateController; @@ -168,6 +196,9 @@ public class LockIconViewController extends ViewController<LockIconView> impleme mExecutor = executor; mVibrator = vibrator; mAuthRippleController = authRippleController; + mTransitionInteractor = transitionInteractor; + mKeyguardInteractor = keyguardInteractor; + mFeatureFlags = featureFlags; mMaxBurnInOffsetX = resources.getDimensionPixelSize(R.dimen.udfps_burn_in_offset_x); mMaxBurnInOffsetY = resources.getDimensionPixelSize(R.dimen.udfps_burn_in_offset_y); @@ -184,6 +215,12 @@ public class LockIconViewController extends ViewController<LockIconView> impleme @Override protected void onInit() { mView.setAccessibilityDelegate(mAccessibilityDelegate); + + if (mFeatureFlags.isEnabled(DOZING_MIGRATION_1)) { + collectFlow(mView, mTransitionInteractor.getDozeAmountTransition(), + mDozeTransitionCallback); + collectFlow(mView, mKeyguardInteractor.isDozing(), mIsDozingCallback); + } } @Override @@ -379,14 +416,17 @@ public class LockIconViewController extends ViewController<LockIconView> impleme pw.println(" mShowUnlockIcon: " + mShowUnlockIcon); pw.println(" mShowLockIcon: " + mShowLockIcon); pw.println(" mShowAodUnlockedIcon: " + mShowAodUnlockedIcon); - pw.println(" mIsDozing: " + mIsDozing); - pw.println(" mIsBouncerShowing: " + mIsBouncerShowing); - pw.println(" mUserUnlockedWithBiometric: " + mUserUnlockedWithBiometric); - pw.println(" mRunningFPS: " + mRunningFPS); - pw.println(" mCanDismissLockScreen: " + mCanDismissLockScreen); - pw.println(" mStatusBarState: " + StatusBarState.toString(mStatusBarState)); - pw.println(" mInterpolatedDarkAmount: " + mInterpolatedDarkAmount); - pw.println(" mSensorTouchLocation: " + mSensorTouchLocation); + pw.println(); + pw.println(" mIsDozing: " + mIsDozing); + pw.println(" isFlagEnabled(DOZING_MIGRATION_1): " + + mFeatureFlags.isEnabled(DOZING_MIGRATION_1)); + pw.println(" mIsBouncerShowing: " + mIsBouncerShowing); + pw.println(" mUserUnlockedWithBiometric: " + mUserUnlockedWithBiometric); + pw.println(" mRunningFPS: " + mRunningFPS); + pw.println(" mCanDismissLockScreen: " + mCanDismissLockScreen); + pw.println(" mStatusBarState: " + StatusBarState.toString(mStatusBarState)); + pw.println(" mInterpolatedDarkAmount: " + mInterpolatedDarkAmount); + pw.println(" mSensorTouchLocation: " + mSensorTouchLocation); if (mView != null) { mView.dump(pw, args); @@ -427,16 +467,20 @@ public class LockIconViewController extends ViewController<LockIconView> impleme new StatusBarStateController.StateListener() { @Override public void onDozeAmountChanged(float linear, float eased) { - mInterpolatedDarkAmount = eased; - mView.setDozeAmount(eased); - updateBurnInOffsets(); + if (!mFeatureFlags.isEnabled(DOZING_MIGRATION_1)) { + mInterpolatedDarkAmount = eased; + mView.setDozeAmount(eased); + updateBurnInOffsets(); + } } @Override public void onDozingChanged(boolean isDozing) { - mIsDozing = isDozing; - updateBurnInOffsets(); - updateVisibility(); + if (!mFeatureFlags.isEnabled(DOZING_MIGRATION_1)) { + mIsDozing = isDozing; + updateBurnInOffsets(); + updateVisibility(); + } } @Override diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt index 2f79e30a0b5b..3308f5550bfc 100644 --- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt +++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt @@ -17,9 +17,12 @@ package com.android.keyguard.logging import android.hardware.biometrics.BiometricConstants.LockoutMode +import android.os.PowerManager +import android.os.PowerManager.WakeReason import android.telephony.ServiceState import android.telephony.SubscriptionInfo import com.android.keyguard.ActiveUnlockConfig +import com.android.keyguard.FaceAuthUiEvent import com.android.keyguard.KeyguardListenModel import com.android.keyguard.KeyguardUpdateMonitorCallback import com.android.systemui.plugins.log.LogBuffer @@ -108,11 +111,10 @@ class KeyguardUpdateMonitorLogger @Inject constructor( }, { "Face help received, msgId: $int1 msg: $str1" }) } - fun logFaceAuthRequested(userInitiatedRequest: Boolean, reason: String?) { + fun logFaceAuthRequested(reason: String?) { logBuffer.log(TAG, DEBUG, { - bool1 = userInitiatedRequest str1 = reason - }, { "requestFaceAuth() userInitiated=$bool1 reason=$str1" }) + }, { "requestFaceAuth() reason=$str1" }) } fun logFaceAuthSuccess(userId: Int) { @@ -269,11 +271,19 @@ class KeyguardUpdateMonitorLogger @Inject constructor( logBuffer.log(TAG, VERBOSE, { int1 = subId }, { "reportSimUnlocked(subId=$int1)" }) } - fun logStartedListeningForFace(faceRunningState: Int, faceAuthReason: String) { + fun logStartedListeningForFace(faceRunningState: Int, faceAuthUiEvent: FaceAuthUiEvent) { logBuffer.log(TAG, VERBOSE, { int1 = faceRunningState - str1 = faceAuthReason - }, { "startListeningForFace(): $int1, reason: $str1" }) + str1 = faceAuthUiEvent.reason + str2 = faceAuthUiEvent.extraInfoToString() + }, { "startListeningForFace(): $int1, reason: $str1 $str2" }) + } + + fun logStartedListeningForFaceFromWakeUp(faceRunningState: Int, @WakeReason pmWakeReason: Int) { + logBuffer.log(TAG, VERBOSE, { + int1 = faceRunningState + str1 = PowerManager.wakeReasonToString(pmWakeReason) + }, { "startListeningForFace(): $int1, reason: wakeUp-$str1" }) } fun logStoppedListeningForFace(faceRunningState: Int, faceAuthReason: String) { @@ -383,4 +393,10 @@ class KeyguardUpdateMonitorLogger @Inject constructor( }, { "#update secure=$bool1 canDismissKeyguard=$bool2" + " trusted=$bool3 trustManaged=$bool4" }) } + + fun logSkipUpdateFaceListeningOnWakeup(@WakeReason pmWakeReason: Int) { + logBuffer.log(TAG, VERBOSE, { + str1 = PowerManager.wakeReasonToString(pmWakeReason) + }, { "Skip updating face listening state on wakeup from $str1"}) + } } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java index ea334b27fa09..777d10c7acfd 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java @@ -28,6 +28,7 @@ import android.os.UserHandle; import android.text.TextUtils; import android.view.Display; import android.view.WindowManager; +import android.view.accessibility.AccessibilityManager; import androidx.annotation.MainThread; @@ -56,6 +57,7 @@ public class AccessibilityFloatingMenuController implements private Context mContext; private final WindowManager mWindowManager; private final DisplayManager mDisplayManager; + private final AccessibilityManager mAccessibilityManager; private final FeatureFlags mFeatureFlags; @VisibleForTesting IAccessibilityFloatingMenu mFloatingMenu; @@ -96,6 +98,7 @@ public class AccessibilityFloatingMenuController implements public AccessibilityFloatingMenuController(Context context, WindowManager windowManager, DisplayManager displayManager, + AccessibilityManager accessibilityManager, AccessibilityButtonTargetsObserver accessibilityButtonTargetsObserver, AccessibilityButtonModeObserver accessibilityButtonModeObserver, KeyguardUpdateMonitor keyguardUpdateMonitor, @@ -103,6 +106,7 @@ public class AccessibilityFloatingMenuController implements mContext = context; mWindowManager = windowManager; mDisplayManager = displayManager; + mAccessibilityManager = accessibilityManager; mAccessibilityButtonTargetsObserver = accessibilityButtonTargetsObserver; mAccessibilityButtonModeObserver = accessibilityButtonModeObserver; mKeyguardUpdateMonitor = keyguardUpdateMonitor; @@ -180,7 +184,8 @@ public class AccessibilityFloatingMenuController implements final Display defaultDisplay = mDisplayManager.getDisplay(DEFAULT_DISPLAY); mFloatingMenu = new MenuViewLayerController( mContext.createWindowContext(defaultDisplay, - TYPE_NAVIGATION_BAR_PANEL, /* options= */ null), mWindowManager); + TYPE_NAVIGATION_BAR_PANEL, /* options= */ null), mWindowManager, + mAccessibilityManager); } else { mFloatingMenu = new AccessibilityFloatingMenu(mContext); } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationController.java new file mode 100644 index 000000000000..ee048e1a02d3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationController.java @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.accessibility.floatingmenu; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.content.ComponentCallbacks; +import android.content.res.Configuration; +import android.view.MotionEvent; + +import androidx.annotation.NonNull; +import androidx.dynamicanimation.animation.DynamicAnimation; + +import com.android.systemui.R; +import com.android.wm.shell.bubbles.DismissView; +import com.android.wm.shell.common.magnetictarget.MagnetizedObject; + +/** + * Controls the interaction between {@link MagnetizedObject} and + * {@link MagnetizedObject.MagneticTarget}. + */ +class DismissAnimationController implements ComponentCallbacks { + private static final float COMPLETELY_OPAQUE = 1.0f; + private static final float COMPLETELY_TRANSPARENT = 0.0f; + private static final float CIRCLE_VIEW_DEFAULT_SCALE = 1.0f; + private static final float ANIMATING_MAX_ALPHA = 0.7f; + + private final DismissView mDismissView; + private final MenuView mMenuView; + private final ValueAnimator mDismissAnimator; + private final MagnetizedObject<?> mMagnetizedObject; + private float mMinDismissSize; + private float mSizePercent; + + DismissAnimationController(DismissView dismissView, MenuView menuView) { + mDismissView = dismissView; + mDismissView.setPivotX(dismissView.getWidth() / 2.0f); + mDismissView.setPivotY(dismissView.getHeight() / 2.0f); + mMenuView = menuView; + + updateResources(); + + mDismissAnimator = ValueAnimator.ofFloat(COMPLETELY_OPAQUE, COMPLETELY_TRANSPARENT); + mDismissAnimator.addUpdateListener(dismissAnimation -> { + final float animatedValue = (float) dismissAnimation.getAnimatedValue(); + final float scaleValue = Math.max(animatedValue, mSizePercent); + dismissView.getCircle().setScaleX(scaleValue); + dismissView.getCircle().setScaleY(scaleValue); + + menuView.setAlpha(Math.max(animatedValue, ANIMATING_MAX_ALPHA)); + }); + + mDismissAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(@NonNull Animator animation, boolean isReverse) { + super.onAnimationEnd(animation, isReverse); + + if (isReverse) { + mDismissView.getCircle().setScaleX(CIRCLE_VIEW_DEFAULT_SCALE); + mDismissView.getCircle().setScaleY(CIRCLE_VIEW_DEFAULT_SCALE); + mMenuView.setAlpha(COMPLETELY_OPAQUE); + } + } + }); + + mMagnetizedObject = + new MagnetizedObject<MenuView>(mMenuView.getContext(), mMenuView, + new MenuAnimationController.MenuPositionProperty( + DynamicAnimation.TRANSLATION_X), + new MenuAnimationController.MenuPositionProperty( + DynamicAnimation.TRANSLATION_Y)) { + @Override + public void getLocationOnScreen(MenuView underlyingObject, int[] loc) { + underlyingObject.getLocationOnScreen(loc); + } + + @Override + public float getHeight(MenuView underlyingObject) { + return underlyingObject.getHeight(); + } + + @Override + public float getWidth(MenuView underlyingObject) { + return underlyingObject.getWidth(); + } + }; + + final MagnetizedObject.MagneticTarget magneticTarget = new MagnetizedObject.MagneticTarget( + dismissView.getCircle(), (int) mMinDismissSize); + mMagnetizedObject.addTarget(magneticTarget); + } + + @Override + public void onConfigurationChanged(@NonNull Configuration newConfig) { + updateResources(); + } + + @Override + public void onLowMemory() { + // Do nothing + } + + void showDismissView(boolean show) { + if (show) { + mDismissView.show(); + } else { + mDismissView.hide(); + } + } + + void setMagnetListener(MagnetizedObject.MagnetListener magnetListener) { + mMagnetizedObject.setMagnetListener(magnetListener); + } + + void maybeConsumeDownMotionEvent(MotionEvent event) { + mMagnetizedObject.maybeConsumeMotionEvent(event); + } + + /** + * This used to pass {@link MotionEvent#ACTION_DOWN} to the magnetized object to check if it was + * within the magnetic field. It should be used in the {@link MenuListViewTouchHandler}. + * + * @param event that move the magnetized object which is also the menu list view. + * @return true if the location of the motion events moves within the magnetic field of a + * target, but false if didn't set + * {@link DismissAnimationController#setMagnetListener(MagnetizedObject.MagnetListener)}. + */ + boolean maybeConsumeMoveMotionEvent(MotionEvent event) { + return mMagnetizedObject.maybeConsumeMotionEvent(event); + } + + /** + * This used to pass {@link MotionEvent#ACTION_UP} to the magnetized object to check if it was + * within the magnetic field. It should be used in the {@link MenuListViewTouchHandler}. + * + * @param event that move the magnetized object which is also the menu list view. + * @return true if the location of the motion events moves within the magnetic field of a + * target, but false if didn't set + * {@link DismissAnimationController#setMagnetListener(MagnetizedObject.MagnetListener)}. + */ + boolean maybeConsumeUpMotionEvent(MotionEvent event) { + return mMagnetizedObject.maybeConsumeMotionEvent(event); + } + + void animateDismissMenu(boolean scaleUp) { + if (scaleUp) { + mDismissAnimator.start(); + } else { + mDismissAnimator.reverse(); + } + } + + private void updateResources() { + final float maxDismissSize = mDismissView.getResources().getDimensionPixelSize( + R.dimen.dismiss_circle_size); + mMinDismissSize = mDismissView.getResources().getDimensionPixelSize( + R.dimen.dismiss_circle_small); + mSizePercent = mMinDismissSize / maxDismissSize; + } + + interface DismissCallback { + void onDismiss(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java index d6d039903505..396f584d76a6 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java @@ -35,6 +35,8 @@ import androidx.dynamicanimation.animation.SpringAnimation; import androidx.dynamicanimation.animation.SpringForce; import androidx.recyclerview.widget.RecyclerView; +import com.android.internal.util.Preconditions; + import java.util.HashMap; /** @@ -47,6 +49,9 @@ class MenuAnimationController { private static final float MIN_PERCENT = 0.0f; private static final float MAX_PERCENT = 1.0f; private static final float COMPLETELY_OPAQUE = 1.0f; + private static final float COMPLETELY_TRANSPARENT = 0.0f; + private static final float SCALE_SHRINK = 0.0f; + private static final float SCALE_GROW = 1.0f; private static final float FLING_FRICTION_SCALAR = 1.9f; private static final float DEFAULT_FRICTION = 4.2f; private static final float SPRING_AFTER_FLING_DAMPING_RATIO = 0.85f; @@ -61,6 +66,7 @@ class MenuAnimationController { private final Handler mHandler; private boolean mIsMovedToEdge; private boolean mIsFadeEffectEnabled; + private DismissAnimationController.DismissCallback mDismissCallback; // Cache the animations state of {@link DynamicAnimation.TRANSLATION_X} and {@link // DynamicAnimation.TRANSLATION_Y} to be well controlled by the touch handler @@ -99,6 +105,11 @@ class MenuAnimationController { } } + void setDismissCallback( + DismissAnimationController.DismissCallback dismissCallback) { + mDismissCallback = dismissCallback; + } + void moveToTopLeftPosition() { mIsMovedToEdge = false; final Rect draggableBounds = mMenuView.getMenuDraggableBounds(); @@ -129,6 +140,13 @@ class MenuAnimationController { constrainPositionAndUpdate(position); } + void removeMenu() { + Preconditions.checkArgument(mDismissCallback != null, + "The dismiss callback should be initialized first."); + + mDismissCallback.onDismiss(); + } + void flingMenuThenSpringToEdge(float x, float velocityX, float velocityY) { final boolean shouldMenuFlingLeft = isOnLeftSide() ? velocityX < ESCAPE_VELOCITY @@ -297,6 +315,28 @@ class MenuAnimationController { mMenuView.onDraggingStart(); } + void startShrinkAnimation(Runnable endAction) { + mMenuView.animate().cancel(); + + mMenuView.animate() + .scaleX(SCALE_SHRINK) + .scaleY(SCALE_SHRINK) + .alpha(COMPLETELY_TRANSPARENT) + .translationY(mMenuView.getTranslationY()) + .withEndAction(endAction).start(); + } + + void startGrowAnimation() { + mMenuView.animate().cancel(); + + mMenuView.animate() + .scaleX(SCALE_GROW) + .scaleY(SCALE_GROW) + .alpha(COMPLETELY_OPAQUE) + .translationY(mMenuView.getTranslationY()) + .start(); + } + private void onSpringAnimationEnd(PointF position) { mMenuView.onBoundsInParentChanged((int) position.x, (int) position.y); constrainPositionAndUpdate(position); diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegate.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegate.java index e69a24810fdc..ac5736b0c26d 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegate.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegate.java @@ -84,6 +84,12 @@ class MenuItemAccessibilityDelegate extends RecyclerViewAccessibilityDelegate.It new AccessibilityNodeInfoCompat.AccessibilityActionCompat(moveEdgeId, res.getString(moveEdgeTextResId)); info.addAction(moveToOrOutEdge); + + final AccessibilityNodeInfoCompat.AccessibilityActionCompat removeMenu = + new AccessibilityNodeInfoCompat.AccessibilityActionCompat( + R.id.action_remove_menu, + res.getString(R.string.accessibility_floating_button_action_remove_menu)); + info.addAction(removeMenu); } @Override @@ -126,6 +132,11 @@ class MenuItemAccessibilityDelegate extends RecyclerViewAccessibilityDelegate.It return true; } + if (action == R.id.action_remove_menu) { + mAnimationController.removeMenu(); + return true; + } + return super.performAccessibilityAction(host, action, args); } } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandler.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandler.java index 3146c9f0d2af..bc3cf0a6bab0 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandler.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandler.java @@ -38,9 +38,12 @@ class MenuListViewTouchHandler implements RecyclerView.OnItemTouchListener { private final PointF mMenuTranslationDown = new PointF(); private boolean mIsDragging = false; private float mTouchSlop; + private final DismissAnimationController mDismissAnimationController; - MenuListViewTouchHandler(MenuAnimationController menuAnimationController) { + MenuListViewTouchHandler(MenuAnimationController menuAnimationController, + DismissAnimationController dismissAnimationController) { mMenuAnimationController = menuAnimationController; + mDismissAnimationController = dismissAnimationController; } @Override @@ -61,6 +64,7 @@ class MenuListViewTouchHandler implements RecyclerView.OnItemTouchListener { mMenuTranslationDown.set(menuView.getTranslationX(), menuView.getTranslationY()); mMenuAnimationController.cancelAnimations(); + mDismissAnimationController.maybeConsumeDownMotionEvent(motionEvent); break; case MotionEvent.ACTION_MOVE: if (mIsDragging || Math.hypot(dx, dy) > mTouchSlop) { @@ -69,8 +73,13 @@ class MenuListViewTouchHandler implements RecyclerView.OnItemTouchListener { mMenuAnimationController.onDraggingStart(); } - mMenuAnimationController.moveToPositionX(mMenuTranslationDown.x + dx); - mMenuAnimationController.moveToPositionYIfNeeded(mMenuTranslationDown.y + dy); + mDismissAnimationController.showDismissView(/* show= */ true); + + if (!mDismissAnimationController.maybeConsumeMoveMotionEvent(motionEvent)) { + mMenuAnimationController.moveToPositionX(mMenuTranslationDown.x + dx); + mMenuAnimationController.moveToPositionYIfNeeded( + mMenuTranslationDown.y + dy); + } } break; case MotionEvent.ACTION_UP: @@ -79,10 +88,18 @@ class MenuListViewTouchHandler implements RecyclerView.OnItemTouchListener { final float endX = mMenuTranslationDown.x + dx; mIsDragging = false; - if (!mMenuAnimationController.maybeMoveToEdgeAndHide(endX)) { + if (mMenuAnimationController.maybeMoveToEdgeAndHide(endX)) { + mDismissAnimationController.showDismissView(/* show= */ false); + mMenuAnimationController.fadeOutIfEnabled(); + + return true; + } + + if (!mDismissAnimationController.maybeConsumeUpMotionEvent(motionEvent)) { mVelocityTracker.computeCurrentVelocity(VELOCITY_UNIT_SECONDS); mMenuAnimationController.flingMenuThenSpringToEdge(endX, mVelocityTracker.getXVelocity(), mVelocityTracker.getYVelocity()); + mDismissAnimationController.showDismissView(/* show= */ false); } // Avoid triggering the listener of the item. diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuMessageView.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuMessageView.java new file mode 100644 index 000000000000..9875ad06f1ed --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuMessageView.java @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.accessibility.floatingmenu; + +import static android.util.TypedValue.COMPLEX_UNIT_PX; +import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; + +import android.annotation.IntDef; +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.Rect; +import android.view.Gravity; +import android.view.View; +import android.view.ViewTreeObserver; +import android.widget.Button; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.android.settingslib.Utils; +import com.android.systemui.R; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * The message view with the action prompt to whether to undo operation for users when removing + * the {@link MenuView}. + */ +class MenuMessageView extends LinearLayout implements + ViewTreeObserver.OnComputeInternalInsetsListener { + private final TextView mTextView; + private final Button mUndoButton; + + @IntDef({ + Index.TEXT_VIEW, + Index.UNDO_BUTTON + }) + @Retention(RetentionPolicy.SOURCE) + @interface Index { + int TEXT_VIEW = 0; + int UNDO_BUTTON = 1; + } + + MenuMessageView(Context context) { + super(context); + + setVisibility(GONE); + + mTextView = new TextView(context); + mUndoButton = new Button(context); + + addView(mTextView, Index.TEXT_VIEW, + new LayoutParams(/* width= */ 0, WRAP_CONTENT, /* weight= */ 1)); + addView(mUndoButton, Index.UNDO_BUTTON, new LayoutParams(WRAP_CONTENT, WRAP_CONTENT)); + } + + @Override + protected void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + + updateResources(); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + final FrameLayout.LayoutParams containerParams = new FrameLayout.LayoutParams(WRAP_CONTENT, + WRAP_CONTENT); + containerParams.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL; + setLayoutParams(containerParams); + setGravity(Gravity.CENTER_VERTICAL); + + mUndoButton.setBackground(null); + + updateResources(); + + getViewTreeObserver().addOnComputeInternalInsetsListener(this); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + + getViewTreeObserver().removeOnComputeInternalInsetsListener(this); + } + + @Override + public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) { + inoutInfo.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); + + if (getVisibility() == VISIBLE) { + final int x = (int) getX(); + final int y = (int) getY(); + inoutInfo.touchableRegion.union(new Rect(x, y, x + getWidth(), y + getHeight())); + } + } + + /** + * Registers a listener to be invoked when this undo action button is clicked. It should be + * called after {@link View#onAttachedToWindow()}. + * + * @param listener The listener that will run + */ + void setUndoListener(OnClickListener listener) { + mUndoButton.setOnClickListener(listener); + } + + private void updateResources() { + final Resources res = getResources(); + + final int containerPadding = + res.getDimensionPixelSize( + R.dimen.accessibility_floating_menu_message_container_horizontal_padding); + final int margin = res.getDimensionPixelSize( + R.dimen.accessibility_floating_menu_message_margin); + final FrameLayout.LayoutParams containerParams = + (FrameLayout.LayoutParams) getLayoutParams(); + containerParams.setMargins(margin, margin, margin, margin); + setLayoutParams(containerParams); + setBackground(res.getDrawable(R.drawable.accessibility_floating_message_background)); + setPadding(containerPadding, /* top= */ 0, containerPadding, /* bottom= */ 0); + setMinimumWidth( + res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_message_min_width)); + setMinimumHeight( + res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_message_min_height)); + setElevation( + res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_message_elevation)); + + final int textPadding = + res.getDimensionPixelSize( + R.dimen.accessibility_floating_menu_message_text_vertical_padding); + final int textColor = res.getColor(R.color.accessibility_floating_menu_message_text); + final int textSize = res.getDimensionPixelSize( + R.dimen.accessibility_floating_menu_message_text_size); + mTextView.setPadding(/* left= */ 0, textPadding, /* right= */ 0, textPadding); + mTextView.setTextSize(COMPLEX_UNIT_PX, textSize); + mTextView.setTextColor(textColor); + + final ColorStateList colorAccent = Utils.getColorAccent(getContext()); + mUndoButton.setText(res.getString(R.string.accessibility_floating_button_undo)); + mUndoButton.setTextSize(COMPLEX_UNIT_PX, textSize); + mUndoButton.setTextColor(colorAccent); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java index 15d139cf15da..6a14af52fbaf 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java @@ -42,7 +42,7 @@ import java.util.Collections; import java.util.List; /** - * The menu view displays the accessibility features. + * The container view displays the accessibility features. */ @SuppressLint("ViewConstructor") class MenuView extends FrameLayout implements @@ -64,13 +64,14 @@ class MenuView extends FrameLayout implements this::onTargetFeaturesChanged; private final MenuViewAppearance mMenuViewAppearance; + private OnTargetFeaturesChangeListener mFeaturesChangeListener; + MenuView(Context context, MenuViewModel menuViewModel, MenuViewAppearance menuViewAppearance) { super(context); mMenuViewModel = menuViewModel; mMenuViewAppearance = menuViewAppearance; mMenuAnimationController = new MenuAnimationController(this); - mAdapter = new AccessibilityTargetAdapter(mTargetFeatures); mTargetFeaturesView = new RecyclerView(context); mTargetFeaturesView.setAdapter(mAdapter); @@ -96,7 +97,9 @@ class MenuView extends FrameLayout implements @Override public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) { inoutInfo.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); - inoutInfo.touchableRegion.set(mBoundsInParent); + if (getVisibility() == VISIBLE) { + inoutInfo.touchableRegion.union(mBoundsInParent); + } } @Override @@ -108,10 +111,18 @@ class MenuView extends FrameLayout implements mTargetFeaturesView.setOverScrollMode(mMenuViewAppearance.getMenuScrollMode()); } + void setOnTargetFeaturesChangeListener(OnTargetFeaturesChangeListener listener) { + mFeaturesChangeListener = listener; + } + void addOnItemTouchListenerToList(RecyclerView.OnItemTouchListener listener) { mTargetFeaturesView.addOnItemTouchListener(listener); } + MenuAnimationController getMenuAnimationController() { + return mMenuAnimationController; + } + @SuppressLint("NotifyDataSetChanged") private void onItemSizeChanged() { mAdapter.setItemPadding(mMenuViewAppearance.getMenuPadding()); @@ -139,7 +150,7 @@ class MenuView extends FrameLayout implements onEdgeChanged(); } - private void onEdgeChanged() { + void onEdgeChanged() { final int[] insets = mMenuViewAppearance.getMenuInsets(); getContainerViewInsetLayer().setLayerInset(INDEX_MENU_ITEM, insets[0], insets[1], insets[2], insets[3]); @@ -193,6 +204,9 @@ class MenuView extends FrameLayout implements onEdgeChanged(); onPositionChanged(); + if (mFeaturesChangeListener != null) { + mFeaturesChangeListener.onChange(newTargetFeatures); + } mMenuAnimationController.fadeOutIfEnabled(); } @@ -299,4 +313,17 @@ class MenuView extends FrameLayout implements final ViewGroup parentView = (ViewGroup) getParent(); parentView.setSystemGestureExclusionRects(Collections.singletonList(mBoundsInParent)); } + + /** + * Interface definition for the {@link AccessibilityTarget} list changes. + */ + interface OnTargetFeaturesChangeListener { + /** + * Called when the list of accessibility target features was updated. This will be + * invoked when the end of {@code onTargetFeaturesChanged}. + * + * @param newTargetFeatures the list related to the current accessibility features. + */ + void onChange(List<AccessibilityTarget> newTargetFeatures); + } } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java index 5252519e9faf..33e155df80e3 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java @@ -16,42 +16,155 @@ package com.android.systemui.accessibility.floatingmenu; +import static com.android.systemui.accessibility.floatingmenu.MenuMessageView.Index; + import android.annotation.IntDef; import android.annotation.SuppressLint; import android.content.Context; +import android.content.res.Configuration; +import android.os.Handler; +import android.os.Looper; +import android.provider.Settings; +import android.util.PluralsMessageFormatter; import android.view.MotionEvent; import android.view.WindowManager; +import android.view.accessibility.AccessibilityManager; import android.widget.FrameLayout; +import android.widget.TextView; import androidx.annotation.NonNull; +import com.android.internal.accessibility.dialog.AccessibilityTarget; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.Preconditions; +import com.android.systemui.R; +import com.android.wm.shell.bubbles.DismissView; +import com.android.wm.shell.common.magnetictarget.MagnetizedObject; + import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.HashMap; +import java.util.List; +import java.util.Map; /** - * The basic interactions with the child view {@link MenuView}. + * The basic interactions with the child views {@link MenuView}, {@link DismissView}, and + * {@link MenuMessageView}. When dragging the menu view, the dismissed view would be shown at the + * same time. If the menu view overlaps on the dismissed circle view and drops out, the menu + * message view would be shown and allowed users to undo it. */ @SuppressLint("ViewConstructor") class MenuViewLayer extends FrameLayout { + private static final int SHOW_MESSAGE_DELAY_MS = 3000; + private final MenuView mMenuView; + private final MenuMessageView mMessageView; + private final DismissView mDismissView; + private final MenuAnimationController mMenuAnimationController; + private final AccessibilityManager mAccessibilityManager; + private final Handler mHandler = new Handler(Looper.getMainLooper()); + private final IAccessibilityFloatingMenu mFloatingMenu; + private final DismissAnimationController mDismissAnimationController; @IntDef({ - LayerIndex.MENU_VIEW + LayerIndex.MENU_VIEW, + LayerIndex.DISMISS_VIEW, + LayerIndex.MESSAGE_VIEW, }) @Retention(RetentionPolicy.SOURCE) @interface LayerIndex { int MENU_VIEW = 0; + int DISMISS_VIEW = 1; + int MESSAGE_VIEW = 2; } - MenuViewLayer(@NonNull Context context, WindowManager windowManager) { + @VisibleForTesting + final Runnable mDismissMenuAction = new Runnable() { + @Override + public void run() { + Settings.Secure.putString(getContext().getContentResolver(), + Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, /* value= */ ""); + mFloatingMenu.hide(); + } + }; + + MenuViewLayer(@NonNull Context context, WindowManager windowManager, + AccessibilityManager accessibilityManager, IAccessibilityFloatingMenu floatingMenu) { super(context); + mAccessibilityManager = accessibilityManager; + mFloatingMenu = floatingMenu; + final MenuViewModel menuViewModel = new MenuViewModel(context); final MenuViewAppearance menuViewAppearance = new MenuViewAppearance(context, windowManager); mMenuView = new MenuView(context, menuViewModel, menuViewAppearance); + mMenuAnimationController = mMenuView.getMenuAnimationController(); + mMenuAnimationController.setDismissCallback(this::hideMenuAndShowMessage); + + mDismissView = new DismissView(context); + mDismissAnimationController = new DismissAnimationController(mDismissView, mMenuView); + mDismissAnimationController.setMagnetListener(new MagnetizedObject.MagnetListener() { + @Override + public void onStuckToTarget(@NonNull MagnetizedObject.MagneticTarget target) { + mDismissAnimationController.animateDismissMenu(/* scaleUp= */ true); + } + + @Override + public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target, + float velocityX, float velocityY, boolean wasFlungOut) { + mDismissAnimationController.animateDismissMenu(/* scaleUp= */ false); + } + + @Override + public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) { + hideMenuAndShowMessage(); + mDismissView.hide(); + mDismissAnimationController.animateDismissMenu(/* scaleUp= */ false); + } + }); + + final MenuListViewTouchHandler menuListViewTouchHandler = new MenuListViewTouchHandler( + mMenuAnimationController, mDismissAnimationController); + mMenuView.addOnItemTouchListenerToList(menuListViewTouchHandler); + + mMessageView = new MenuMessageView(context); + + mMenuView.setOnTargetFeaturesChangeListener(newTargetFeatures -> { + if (newTargetFeatures.size() < 1) { + return; + } + + // During the undo action period, the pending action will be canceled and undo back + // to the previous state if users did any action related to the accessibility features. + if (mMessageView.getVisibility() == VISIBLE) { + undo(); + } + + final TextView messageText = (TextView) mMessageView.getChildAt(Index.TEXT_VIEW); + messageText.setText(getMessageText(newTargetFeatures)); + }); addView(mMenuView, LayerIndex.MENU_VIEW); + addView(mDismissView, LayerIndex.DISMISS_VIEW); + addView(mMessageView, LayerIndex.MESSAGE_VIEW); + } + + @Override + protected void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + mDismissView.updateResources(); + } + + private String getMessageText(List<AccessibilityTarget> newTargetFeatures) { + Preconditions.checkArgument(newTargetFeatures.size() > 0, + "The list should at least have one feature."); + + final Map<String, Object> arguments = new HashMap<>(); + arguments.put("count", newTargetFeatures.size()); + arguments.put("label", newTargetFeatures.get(0).getLabel()); + return PluralsMessageFormatter.format(getResources(), arguments, + R.string.accessibility_floating_button_undo_message_text); } @Override @@ -68,6 +181,8 @@ class MenuViewLayer extends FrameLayout { super.onAttachedToWindow(); mMenuView.show(); + mMessageView.setUndoListener(view -> undo()); + mContext.registerComponentCallbacks(mDismissAnimationController); } @Override @@ -75,5 +190,26 @@ class MenuViewLayer extends FrameLayout { super.onDetachedFromWindow(); mMenuView.hide(); + mHandler.removeCallbacksAndMessages(/* token= */ null); + mContext.unregisterComponentCallbacks(mDismissAnimationController); + } + + private void hideMenuAndShowMessage() { + final int delayTime = mAccessibilityManager.getRecommendedTimeoutMillis( + SHOW_MESSAGE_DELAY_MS, + AccessibilityManager.FLAG_CONTENT_TEXT + | AccessibilityManager.FLAG_CONTENT_CONTROLS); + mHandler.postDelayed(mDismissMenuAction, delayTime); + mMessageView.setVisibility(VISIBLE); + mMenuAnimationController.startShrinkAnimation(() -> mMenuView.setVisibility(GONE)); + } + + private void undo() { + mHandler.removeCallbacksAndMessages(/* token= */ null); + mMessageView.setVisibility(GONE); + mMenuView.onEdgeChanged(); + mMenuView.onPositionChanged(); + mMenuView.setVisibility(VISIBLE); + mMenuAnimationController.startGrowAnimation(); } } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java index d2093c200ca2..b1a64eda46ff 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java @@ -22,6 +22,7 @@ import android.content.Context; import android.graphics.PixelFormat; import android.view.WindowInsets; import android.view.WindowManager; +import android.view.accessibility.AccessibilityManager; /** * Controls the {@link MenuViewLayer} whether to be attached to the window via the interface @@ -32,9 +33,10 @@ class MenuViewLayerController implements IAccessibilityFloatingMenu { private final MenuViewLayer mMenuViewLayer; private boolean mIsShowing; - MenuViewLayerController(Context context, WindowManager windowManager) { + MenuViewLayerController(Context context, WindowManager windowManager, + AccessibilityManager accessibilityManager) { mWindowManager = windowManager; - mMenuViewLayer = new MenuViewLayer(context, windowManager); + mMenuViewLayer = new MenuViewLayer(context, windowManager, accessibilityManager, this); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java index b50bfd7c24f9..f74c721bf114 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java @@ -26,6 +26,7 @@ import android.annotation.DurationMillisLong; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.AlertDialog; import android.content.Context; import android.graphics.PixelFormat; import android.hardware.biometrics.BiometricAuthenticator.Modality; @@ -63,6 +64,9 @@ import com.android.internal.widget.LockPatternUtils; import com.android.systemui.R; import com.android.systemui.animation.Interpolators; import com.android.systemui.biometrics.AuthController.ScaleFactorProvider; +import com.android.systemui.biometrics.domain.interactor.BiometricPromptCredentialInteractor; +import com.android.systemui.biometrics.ui.CredentialView; +import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.util.concurrency.DelayableExecutor; @@ -74,11 +78,13 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import javax.inject.Provider; + /** * Top level container/controller for the BiometricPrompt UI. */ public class AuthContainerView extends LinearLayout - implements AuthDialog, WakefulnessLifecycle.Observer { + implements AuthDialog, WakefulnessLifecycle.Observer, CredentialView.Host { private static final String TAG = "AuthContainerView"; @@ -112,15 +118,18 @@ public class AuthContainerView extends LinearLayout private final IBinder mWindowToken = new Binder(); private final WindowManager mWindowManager; private final Interpolator mLinearOutSlowIn; - private final CredentialCallback mCredentialCallback; private final LockPatternUtils mLockPatternUtils; private final WakefulnessLifecycle mWakefulnessLifecycle; private final InteractionJankMonitor mInteractionJankMonitor; + // TODO: these should be migrated out once ready + private final Provider<BiometricPromptCredentialInteractor> mBiometricPromptInteractor; + private final Provider<CredentialViewModel> mCredentialViewModelProvider; + @VisibleForTesting final BiometricCallback mBiometricCallback; @Nullable private AuthBiometricView mBiometricView; - @Nullable private AuthCredentialView mCredentialView; + @Nullable private View mCredentialView; private final AuthPanelController mPanelController; private final FrameLayout mFrameLayout; private final ImageView mBackgroundView; @@ -229,11 +238,13 @@ public class AuthContainerView extends LinearLayout @NonNull WakefulnessLifecycle wakefulnessLifecycle, @NonNull UserManager userManager, @NonNull LockPatternUtils lockPatternUtils, - @NonNull InteractionJankMonitor jankMonitor) { + @NonNull InteractionJankMonitor jankMonitor, + @NonNull Provider<BiometricPromptCredentialInteractor> biometricPromptInteractor, + @NonNull Provider<CredentialViewModel> credentialViewModelProvider) { mConfig.mSensorIds = sensorIds; return new AuthContainerView(mConfig, fpProps, faceProps, wakefulnessLifecycle, - userManager, lockPatternUtils, jankMonitor, new Handler(Looper.getMainLooper()), - bgExecutor); + userManager, lockPatternUtils, jankMonitor, biometricPromptInteractor, + credentialViewModelProvider, new Handler(Looper.getMainLooper()), bgExecutor); } } @@ -271,12 +282,49 @@ public class AuthContainerView extends LinearLayout } } - final class CredentialCallback implements AuthCredentialView.Callback { - @Override - public void onCredentialMatched(byte[] attestation) { - mCredentialAttestation = attestation; - animateAway(AuthDialogCallback.DISMISSED_CREDENTIAL_AUTHENTICATED); - } + @Override + public void onCredentialMatched(@NonNull byte[] attestation) { + mCredentialAttestation = attestation; + animateAway(AuthDialogCallback.DISMISSED_CREDENTIAL_AUTHENTICATED); + } + + @Override + public void onCredentialAborted() { + sendEarlyUserCanceled(); + animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED); + } + + @Override + public void onCredentialAttemptsRemaining(int remaining, @NonNull String messageBody) { + // Only show dialog if <=1 attempts are left before wiping. + if (remaining == 1) { + showLastAttemptBeforeWipeDialog(messageBody); + } else if (remaining <= 0) { + showNowWipingDialog(messageBody); + } + } + + private void showLastAttemptBeforeWipeDialog(@NonNull String messageBody) { + final AlertDialog alertDialog = new AlertDialog.Builder(mContext) + .setTitle(R.string.biometric_dialog_last_attempt_before_wipe_dialog_title) + .setMessage(messageBody) + .setPositiveButton(android.R.string.ok, null) + .create(); + alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL); + alertDialog.show(); + } + + private void showNowWipingDialog(@NonNull String messageBody) { + final AlertDialog alertDialog = new AlertDialog.Builder(mContext) + .setMessage(messageBody) + .setPositiveButton( + com.android.settingslib.R.string.failed_attempts_now_wiping_dialog_dismiss, + null /* OnClickListener */) + .setOnDismissListener( + dialog -> animateAway(AuthDialogCallback.DISMISSED_ERROR)) + .create(); + alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL); + alertDialog.show(); } @VisibleForTesting @@ -287,6 +335,8 @@ public class AuthContainerView extends LinearLayout @NonNull UserManager userManager, @NonNull LockPatternUtils lockPatternUtils, @NonNull InteractionJankMonitor jankMonitor, + @NonNull Provider<BiometricPromptCredentialInteractor> biometricPromptInteractor, + @NonNull Provider<CredentialViewModel> credentialViewModelProvider, @NonNull Handler mainHandler, @NonNull @Background DelayableExecutor bgExecutor) { super(config.mContext); @@ -302,7 +352,6 @@ public class AuthContainerView extends LinearLayout .getDimension(R.dimen.biometric_dialog_animation_translation_offset); mLinearOutSlowIn = Interpolators.LINEAR_OUT_SLOW_IN; mBiometricCallback = new BiometricCallback(); - mCredentialCallback = new CredentialCallback(); final LayoutInflater layoutInflater = LayoutInflater.from(mContext); mFrameLayout = (FrameLayout) layoutInflater.inflate( @@ -314,6 +363,8 @@ public class AuthContainerView extends LinearLayout mPanelController = new AuthPanelController(mContext, mPanelView); mBackgroundExecutor = bgExecutor; mInteractionJankMonitor = jankMonitor; + mBiometricPromptInteractor = biometricPromptInteractor; + mCredentialViewModelProvider = credentialViewModelProvider; // Inflate biometric view only if necessary. if (Utils.isBiometricAllowed(mConfig.mPromptInfo)) { @@ -404,12 +455,12 @@ public class AuthContainerView extends LinearLayout switch (credentialType) { case Utils.CREDENTIAL_PATTERN: - mCredentialView = (AuthCredentialView) factory.inflate( + mCredentialView = factory.inflate( R.layout.auth_credential_pattern_view, null, false); break; case Utils.CREDENTIAL_PIN: case Utils.CREDENTIAL_PASSWORD: - mCredentialView = (AuthCredentialView) factory.inflate( + mCredentialView = factory.inflate( R.layout.auth_credential_password_view, null, false); break; default: @@ -422,16 +473,12 @@ public class AuthContainerView extends LinearLayout mBackgroundView.setOnClickListener(null); mBackgroundView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); - mCredentialView.setContainerView(this); - mCredentialView.setUserId(mConfig.mUserId); - mCredentialView.setOperationId(mConfig.mOperationId); - mCredentialView.setEffectiveUserId(mEffectiveUserId); - mCredentialView.setCredentialType(credentialType); - mCredentialView.setCallback(mCredentialCallback); - mCredentialView.setPromptInfo(mConfig.mPromptInfo); - mCredentialView.setPanelController(mPanelController, animatePanel); - mCredentialView.setShouldAnimateContents(animateContents); - mCredentialView.setBackgroundExecutor(mBackgroundExecutor); + mBiometricPromptInteractor.get().useCredentialsForAuthentication( + mConfig.mPromptInfo, credentialType, mConfig.mUserId, mConfig.mOperationId); + final CredentialViewModel vm = mCredentialViewModelProvider.get(); + vm.setAnimateContents(animateContents); + ((CredentialView) mCredentialView).init(vm, this, mPanelController, animatePanel); + mFrameLayout.addView(mCredentialView); } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java index 8c7e0efee7e6..313ff4157155 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java @@ -72,6 +72,8 @@ import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.os.SomeArgs; import com.android.internal.widget.LockPatternUtils; import com.android.systemui.CoreStartable; +import com.android.systemui.biometrics.domain.interactor.BiometricPromptCredentialInteractor; +import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; @@ -122,6 +124,10 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, private final Provider<UdfpsController> mUdfpsControllerFactory; private final Provider<SidefpsController> mSidefpsControllerFactory; + // TODO: these should be migrated out once ready + @NonNull private final Provider<BiometricPromptCredentialInteractor> mBiometricPromptInteractor; + @NonNull private final Provider<CredentialViewModel> mCredentialViewModelProvider; + private final Display mDisplay; private float mScaleFactor = 1f; // sensor locations without any resolution scaling nor rotation adjustments: @@ -153,6 +159,7 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, @Nullable private List<FingerprintSensorPropertiesInternal> mSidefpsProps; @NonNull private final SparseBooleanArray mUdfpsEnrolledForUser; + @NonNull private final SparseBooleanArray mFaceEnrolledForUser; @NonNull private final SensorPrivacyManager mSensorPrivacyManager; private final WakefulnessLifecycle mWakefulnessLifecycle; private boolean mAllFingerprintAuthenticatorsRegistered; @@ -349,6 +356,15 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, } } } + if (mFaceProps == null) { + Log.d(TAG, "handleEnrollmentsChanged, mFaceProps is null"); + } else { + for (FaceSensorPropertiesInternal prop : mFaceProps) { + if (prop.sensorId == sensorId) { + mFaceEnrolledForUser.put(userId, hasEnrollments); + } + } + } for (Callback cb : mCallbacks) { cb.onEnrollmentsChanged(modality); } @@ -683,6 +699,8 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, @NonNull LockPatternUtils lockPatternUtils, @NonNull UdfpsLogger udfpsLogger, @NonNull StatusBarStateController statusBarStateController, + @NonNull Provider<BiometricPromptCredentialInteractor> biometricPromptInteractor, + @NonNull Provider<CredentialViewModel> credentialViewModelProvider, @NonNull InteractionJankMonitor jankMonitor, @Main Handler handler, @Background DelayableExecutor bgExecutor, @@ -704,8 +722,12 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, mWindowManager = windowManager; mInteractionJankMonitor = jankMonitor; mUdfpsEnrolledForUser = new SparseBooleanArray(); + mFaceEnrolledForUser = new SparseBooleanArray(); mVibratorHelper = vibrator; + mBiometricPromptInteractor = biometricPromptInteractor; + mCredentialViewModelProvider = credentialViewModelProvider; + mOrientationListener = new BiometricDisplayListener( context, mDisplayManager, @@ -1054,7 +1076,7 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, return false; } - return mFaceManager.hasEnrolledTemplates(userId); + return mFaceEnrolledForUser.get(userId); } /** @@ -1068,6 +1090,11 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, return mUdfpsEnrolledForUser.get(userId); } + /** If BiometricPrompt is currently being shown to the user. */ + public boolean isShowing() { + return mCurrentDialog != null; + } + private void showDialog(SomeArgs args, boolean skipAnimation, Bundle savedState) { mCurrentDialogArgs = args; @@ -1199,7 +1226,8 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, .setMultiSensorConfig(multiSensorConfig) .setScaleFactorProvider(() -> getScaleFactor()) .build(bgExecutor, sensorIds, mFpProps, mFaceProps, wakefulnessLifecycle, - userManager, lockPatternUtils, mInteractionJankMonitor); + userManager, lockPatternUtils, mInteractionJankMonitor, + mBiometricPromptInteractor, mCredentialViewModelProvider); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPasswordView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPasswordView.java deleted file mode 100644 index 76cd3f4c4f1d..000000000000 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPasswordView.java +++ /dev/null @@ -1,238 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.biometrics; - -import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; -import static android.view.WindowInsets.Type.ime; - -import android.annotation.NonNull; -import android.content.Context; -import android.graphics.Insets; -import android.os.UserHandle; -import android.text.InputType; -import android.text.TextUtils; -import android.util.AttributeSet; -import android.view.KeyEvent; -import android.view.View; -import android.view.View.OnApplyWindowInsetsListener; -import android.view.ViewGroup; -import android.view.WindowInsets; -import android.view.inputmethod.EditorInfo; -import android.view.inputmethod.InputMethodManager; -import android.widget.ImeAwareEditText; -import android.widget.TextView; - -import com.android.internal.widget.LockPatternChecker; -import com.android.internal.widget.LockPatternUtils; -import com.android.internal.widget.LockscreenCredential; -import com.android.internal.widget.VerifyCredentialResponse; -import com.android.systemui.Dumpable; -import com.android.systemui.R; - -import java.io.PrintWriter; - -/** - * Pin and Password UI - */ -public class AuthCredentialPasswordView extends AuthCredentialView - implements TextView.OnEditorActionListener, OnApplyWindowInsetsListener, Dumpable { - - private static final String TAG = "BiometricPrompt/AuthCredentialPasswordView"; - - private final InputMethodManager mImm; - private ImeAwareEditText mPasswordField; - private ViewGroup mAuthCredentialHeader; - private ViewGroup mAuthCredentialInput; - private int mBottomInset = 0; - - public AuthCredentialPasswordView(Context context, - AttributeSet attrs) { - super(context, attrs); - mImm = mContext.getSystemService(InputMethodManager.class); - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - - mAuthCredentialHeader = findViewById(R.id.auth_credential_header); - mAuthCredentialInput = findViewById(R.id.auth_credential_input); - mPasswordField = findViewById(R.id.lockPassword); - mPasswordField.setOnEditorActionListener(this); - // TODO: De-dupe the logic with AuthContainerView - mPasswordField.setOnKeyListener((v, keyCode, event) -> { - if (keyCode != KeyEvent.KEYCODE_BACK) { - return false; - } - if (event.getAction() == KeyEvent.ACTION_UP) { - mContainerView.sendEarlyUserCanceled(); - mContainerView.animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED); - } - return true; - }); - - setOnApplyWindowInsetsListener(this); - } - - @Override - protected void onAttachedToWindow() { - super.onAttachedToWindow(); - - mPasswordField.setTextOperationUser(UserHandle.of(mUserId)); - if (mCredentialType == Utils.CREDENTIAL_PIN) { - mPasswordField.setInputType( - InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD); - } - - mPasswordField.requestFocus(); - mPasswordField.scheduleShowSoftInput(); - } - - @Override - public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { - // Check if this was the result of hitting the enter key - final boolean isSoftImeEvent = event == null - && (actionId == EditorInfo.IME_NULL - || actionId == EditorInfo.IME_ACTION_DONE - || actionId == EditorInfo.IME_ACTION_NEXT); - final boolean isKeyboardEnterKey = event != null - && KeyEvent.isConfirmKey(event.getKeyCode()) - && event.getAction() == KeyEvent.ACTION_DOWN; - if (isSoftImeEvent || isKeyboardEnterKey) { - checkPasswordAndUnlock(); - return true; - } - return false; - } - - private void checkPasswordAndUnlock() { - try (LockscreenCredential password = mCredentialType == Utils.CREDENTIAL_PIN - ? LockscreenCredential.createPinOrNone(mPasswordField.getText()) - : LockscreenCredential.createPasswordOrNone(mPasswordField.getText())) { - if (password.isNone()) { - return; - } - - // Request LockSettingsService to return the Gatekeeper Password in the - // VerifyCredentialResponse so that we can request a Gatekeeper HAT with the - // Gatekeeper Password and operationId. - mPendingLockCheck = LockPatternChecker.verifyCredential(mLockPatternUtils, - password, mEffectiveUserId, LockPatternUtils.VERIFY_FLAG_REQUEST_GK_PW_HANDLE, - this::onCredentialVerified); - } - } - - @Override - protected void onCredentialVerified(@NonNull VerifyCredentialResponse response, - int timeoutMs) { - super.onCredentialVerified(response, timeoutMs); - - if (response.isMatched()) { - mImm.hideSoftInputFromWindow(getWindowToken(), 0 /* flags */); - } else { - mPasswordField.setText(""); - } - } - - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - super.onLayout(changed, left, top, right, bottom); - - if (mAuthCredentialInput == null || mAuthCredentialHeader == null || mSubtitleView == null - || mDescriptionView == null || mPasswordField == null || mErrorView == null) { - return; - } - - int inputLeftBound; - int inputTopBound; - int headerRightBound = right; - int headerTopBounds = top; - final int subTitleBottom = (mSubtitleView.getVisibility() == GONE) ? mTitleView.getBottom() - : mSubtitleView.getBottom(); - final int descBottom = (mDescriptionView.getVisibility() == GONE) ? subTitleBottom - : mDescriptionView.getBottom(); - if (getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE) { - inputTopBound = (bottom - mAuthCredentialInput.getHeight()) / 2; - inputLeftBound = (right - left) / 2; - headerRightBound = inputLeftBound; - headerTopBounds -= Math.min(mIconView.getBottom(), mBottomInset); - } else { - inputTopBound = - descBottom + (bottom - descBottom - mAuthCredentialInput.getHeight()) / 2; - inputLeftBound = (right - left - mAuthCredentialInput.getWidth()) / 2; - } - - if (mDescriptionView.getBottom() > mBottomInset) { - mAuthCredentialHeader.layout(left, headerTopBounds, headerRightBound, bottom); - } - mAuthCredentialInput.layout(inputLeftBound, inputTopBound, right, bottom); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - final int newWidth = MeasureSpec.getSize(widthMeasureSpec); - final int newHeight = MeasureSpec.getSize(heightMeasureSpec) - mBottomInset; - - setMeasuredDimension(newWidth, newHeight); - - final int halfWidthSpec = MeasureSpec.makeMeasureSpec(getWidth() / 2, - MeasureSpec.AT_MOST); - final int fullHeightSpec = MeasureSpec.makeMeasureSpec(newHeight, MeasureSpec.UNSPECIFIED); - if (getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE) { - measureChildren(halfWidthSpec, fullHeightSpec); - } else { - measureChildren(widthMeasureSpec, fullHeightSpec); - } - } - - @NonNull - @Override - public WindowInsets onApplyWindowInsets(@NonNull View v, WindowInsets insets) { - - final Insets bottomInset = insets.getInsets(ime()); - if (v instanceof AuthCredentialPasswordView && mBottomInset != bottomInset.bottom) { - mBottomInset = bottomInset.bottom; - if (mBottomInset > 0 - && getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE) { - mTitleView.setSingleLine(true); - mTitleView.setEllipsize(TextUtils.TruncateAt.MARQUEE); - mTitleView.setMarqueeRepeatLimit(-1); - // select to enable marquee unless a screen reader is enabled - mTitleView.setSelected(!mAccessibilityManager.isEnabled() - || !mAccessibilityManager.isTouchExplorationEnabled()); - } else { - mTitleView.setSingleLine(false); - mTitleView.setEllipsize(null); - // select to enable marquee unless a screen reader is enabled - mTitleView.setSelected(false); - } - requestLayout(); - } - return insets; - } - - @Override - public void dump(@NonNull PrintWriter pw, @NonNull String[] args) { - pw.println(TAG + "State:"); - pw.println(" mBottomInset=" + mBottomInset); - pw.println(" mAuthCredentialHeader size=(" + mAuthCredentialHeader.getWidth() + "," - + mAuthCredentialHeader.getHeight()); - pw.println(" mAuthCredentialInput size=(" + mAuthCredentialInput.getWidth() + "," - + mAuthCredentialInput.getHeight()); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPatternView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPatternView.java deleted file mode 100644 index f9e44a0c1724..000000000000 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPatternView.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.biometrics; - -import android.annotation.NonNull; -import android.content.Context; -import android.util.AttributeSet; - -import com.android.internal.widget.LockPatternChecker; -import com.android.internal.widget.LockPatternUtils; -import com.android.internal.widget.LockPatternView; -import com.android.internal.widget.LockscreenCredential; -import com.android.internal.widget.VerifyCredentialResponse; -import com.android.systemui.R; - -import java.util.List; - -/** - * Pattern UI - */ -public class AuthCredentialPatternView extends AuthCredentialView { - - private LockPatternView mLockPatternView; - - private class UnlockPatternListener implements LockPatternView.OnPatternListener { - - @Override - public void onPatternStart() { - - } - - @Override - public void onPatternCleared() { - - } - - @Override - public void onPatternCellAdded(List<LockPatternView.Cell> pattern) { - - } - - @Override - public void onPatternDetected(List<LockPatternView.Cell> pattern) { - if (mPendingLockCheck != null) { - mPendingLockCheck.cancel(false); - } - - mLockPatternView.setEnabled(false); - - if (pattern.size() < LockPatternUtils.MIN_PATTERN_REGISTER_FAIL) { - // Pattern size is less than the minimum, do not count it as a failed attempt. - onPatternVerified(VerifyCredentialResponse.ERROR, 0 /* timeoutMs */); - return; - } - - try (LockscreenCredential credential = LockscreenCredential.createPattern(pattern)) { - // Request LockSettingsService to return the Gatekeeper Password in the - // VerifyCredentialResponse so that we can request a Gatekeeper HAT with the - // Gatekeeper Password and operationId. - mPendingLockCheck = LockPatternChecker.verifyCredential( - mLockPatternUtils, - credential, - mEffectiveUserId, - LockPatternUtils.VERIFY_FLAG_REQUEST_GK_PW_HANDLE, - this::onPatternVerified); - } - } - - private void onPatternVerified(@NonNull VerifyCredentialResponse response, int timeoutMs) { - AuthCredentialPatternView.this.onCredentialVerified(response, timeoutMs); - if (timeoutMs > 0) { - mLockPatternView.setEnabled(false); - } else { - mLockPatternView.setEnabled(true); - } - } - } - - @Override - protected void onErrorTimeoutFinish() { - super.onErrorTimeoutFinish(); - // select to enable marquee unless a screen reader is enabled - mLockPatternView.setEnabled(!mAccessibilityManager.isEnabled() - || !mAccessibilityManager.isTouchExplorationEnabled()); - } - - public AuthCredentialPatternView(Context context, AttributeSet attrs) { - super(context, attrs); - } - - @Override - protected void onAttachedToWindow() { - super.onAttachedToWindow(); - mLockPatternView = findViewById(R.id.lockPattern); - mLockPatternView.setOnPatternListener(new UnlockPatternListener()); - mLockPatternView.setInStealthMode( - !mLockPatternUtils.isVisiblePatternEnabled(mUserId)); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java deleted file mode 100644 index fa623d146756..000000000000 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java +++ /dev/null @@ -1,565 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.biometrics; - -import static android.app.admin.DevicePolicyResources.Strings.SystemUi.BIOMETRIC_DIALOG_WORK_LOCK_FAILED_ATTEMPTS; -import static android.app.admin.DevicePolicyResources.Strings.SystemUi.BIOMETRIC_DIALOG_WORK_PASSWORD_LAST_ATTEMPT; -import static android.app.admin.DevicePolicyResources.Strings.SystemUi.BIOMETRIC_DIALOG_WORK_PATTERN_LAST_ATTEMPT; -import static android.app.admin.DevicePolicyResources.Strings.SystemUi.BIOMETRIC_DIALOG_WORK_PIN_LAST_ATTEMPT; -import static android.app.admin.DevicePolicyResources.UNDEFINED; - -import android.annotation.IntDef; -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.app.AlertDialog; -import android.app.admin.DevicePolicyManager; -import android.content.Context; -import android.content.pm.UserInfo; -import android.graphics.drawable.Drawable; -import android.hardware.biometrics.BiometricPrompt; -import android.hardware.biometrics.PromptInfo; -import android.os.AsyncTask; -import android.os.CountDownTimer; -import android.os.Handler; -import android.os.Looper; -import android.os.SystemClock; -import android.os.UserManager; -import android.text.TextUtils; -import android.util.AttributeSet; -import android.view.View; -import android.view.WindowManager; -import android.view.accessibility.AccessibilityManager; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.TextView; - -import androidx.annotation.StringRes; - -import com.android.internal.widget.LockPatternUtils; -import com.android.internal.widget.VerifyCredentialResponse; -import com.android.systemui.R; -import com.android.systemui.animation.Interpolators; -import com.android.systemui.dagger.qualifiers.Background; -import com.android.systemui.util.concurrency.DelayableExecutor; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -/** - * Abstract base class for Pin, Pattern, or Password authentication, for - * {@link BiometricPrompt.Builder#setAllowedAuthenticators(int)}} - */ -public abstract class AuthCredentialView extends LinearLayout { - private static final String TAG = "BiometricPrompt/AuthCredentialView"; - private static final int ERROR_DURATION_MS = 3000; - - static final int USER_TYPE_PRIMARY = 1; - static final int USER_TYPE_MANAGED_PROFILE = 2; - static final int USER_TYPE_SECONDARY = 3; - @Retention(RetentionPolicy.SOURCE) - @IntDef({USER_TYPE_PRIMARY, USER_TYPE_MANAGED_PROFILE, USER_TYPE_SECONDARY}) - private @interface UserType {} - - protected final Handler mHandler; - protected final LockPatternUtils mLockPatternUtils; - - protected final AccessibilityManager mAccessibilityManager; - private final UserManager mUserManager; - private final DevicePolicyManager mDevicePolicyManager; - - private PromptInfo mPromptInfo; - private AuthPanelController mPanelController; - private boolean mShouldAnimatePanel; - private boolean mShouldAnimateContents; - - protected TextView mTitleView; - protected TextView mSubtitleView; - protected TextView mDescriptionView; - protected ImageView mIconView; - protected TextView mErrorView; - - protected @Utils.CredentialType int mCredentialType; - protected AuthContainerView mContainerView; - protected Callback mCallback; - protected AsyncTask<?, ?, ?> mPendingLockCheck; - protected int mUserId; - protected long mOperationId; - protected int mEffectiveUserId; - protected ErrorTimer mErrorTimer; - - protected @Background DelayableExecutor mBackgroundExecutor; - - interface Callback { - void onCredentialMatched(byte[] attestation); - } - - protected static class ErrorTimer extends CountDownTimer { - private final TextView mErrorView; - private final Context mContext; - - /** - * @param millisInFuture The number of millis in the future from the call - * to {@link #start()} until the countdown is done and {@link - * #onFinish()} - * is called. - * @param countDownInterval The interval along the way to receive - * {@link #onTick(long)} callbacks. - */ - public ErrorTimer(Context context, long millisInFuture, long countDownInterval, - TextView errorView) { - super(millisInFuture, countDownInterval); - mErrorView = errorView; - mContext = context; - } - - @Override - public void onTick(long millisUntilFinished) { - final int secondsCountdown = (int) (millisUntilFinished / 1000); - mErrorView.setText(mContext.getString( - R.string.biometric_dialog_credential_too_many_attempts, secondsCountdown)); - } - - @Override - public void onFinish() { - if (mErrorView != null) { - mErrorView.setText(""); - } - } - } - - protected final Runnable mClearErrorRunnable = new Runnable() { - @Override - public void run() { - if (mErrorView != null) { - mErrorView.setText(""); - } - } - }; - - public AuthCredentialView(Context context, AttributeSet attrs) { - super(context, attrs); - - mLockPatternUtils = new LockPatternUtils(mContext); - mHandler = new Handler(Looper.getMainLooper()); - mAccessibilityManager = mContext.getSystemService(AccessibilityManager.class); - mUserManager = mContext.getSystemService(UserManager.class); - mDevicePolicyManager = mContext.getSystemService(DevicePolicyManager.class); - } - - protected void showError(String error) { - if (mHandler != null) { - mHandler.removeCallbacks(mClearErrorRunnable); - mHandler.postDelayed(mClearErrorRunnable, ERROR_DURATION_MS); - } - if (mErrorView != null) { - mErrorView.setText(error); - } - } - - private void setTextOrHide(TextView view, CharSequence text) { - if (TextUtils.isEmpty(text)) { - view.setVisibility(View.GONE); - } else { - view.setText(text); - } - - Utils.notifyAccessibilityContentChanged(mAccessibilityManager, this); - } - - private void setText(TextView view, CharSequence text) { - view.setText(text); - } - - void setUserId(int userId) { - mUserId = userId; - } - - void setOperationId(long operationId) { - mOperationId = operationId; - } - - void setEffectiveUserId(int effectiveUserId) { - mEffectiveUserId = effectiveUserId; - } - - void setCredentialType(@Utils.CredentialType int credentialType) { - mCredentialType = credentialType; - } - - void setCallback(Callback callback) { - mCallback = callback; - } - - void setPromptInfo(PromptInfo promptInfo) { - mPromptInfo = promptInfo; - } - - void setPanelController(AuthPanelController panelController, boolean animatePanel) { - mPanelController = panelController; - mShouldAnimatePanel = animatePanel; - } - - void setShouldAnimateContents(boolean animateContents) { - mShouldAnimateContents = animateContents; - } - - void setContainerView(AuthContainerView containerView) { - mContainerView = containerView; - } - - void setBackgroundExecutor(@Background DelayableExecutor bgExecutor) { - mBackgroundExecutor = bgExecutor; - } - - @Override - protected void onAttachedToWindow() { - super.onAttachedToWindow(); - - final CharSequence title = getTitle(mPromptInfo); - setText(mTitleView, title); - setTextOrHide(mSubtitleView, getSubtitle(mPromptInfo)); - setTextOrHide(mDescriptionView, getDescription(mPromptInfo)); - announceForAccessibility(title); - - if (mIconView != null) { - final boolean isManagedProfile = Utils.isManagedProfile(mContext, mEffectiveUserId); - final Drawable image; - if (isManagedProfile) { - image = getResources().getDrawable(R.drawable.auth_dialog_enterprise, - mContext.getTheme()); - } else { - image = getResources().getDrawable(R.drawable.auth_dialog_lock, - mContext.getTheme()); - } - mIconView.setImageDrawable(image); - } - - // Only animate this if we're transitioning from a biometric view. - if (mShouldAnimateContents) { - setTranslationY(getResources() - .getDimension(R.dimen.biometric_dialog_credential_translation_offset)); - setAlpha(0); - - postOnAnimation(() -> { - animate().translationY(0) - .setDuration(AuthDialog.ANIMATE_CREDENTIAL_INITIAL_DURATION_MS) - .alpha(1.f) - .setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN) - .withLayer() - .start(); - }); - } - } - - @Override - protected void onDetachedFromWindow() { - super.onDetachedFromWindow(); - if (mErrorTimer != null) { - mErrorTimer.cancel(); - } - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - mTitleView = findViewById(R.id.title); - mSubtitleView = findViewById(R.id.subtitle); - mDescriptionView = findViewById(R.id.description); - mIconView = findViewById(R.id.icon); - mErrorView = findViewById(R.id.error); - } - - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - super.onLayout(changed, left, top, right, bottom); - - if (mShouldAnimatePanel) { - // Credential view is always full screen. - mPanelController.setUseFullScreen(true); - mPanelController.updateForContentDimensions(mPanelController.getContainerWidth(), - mPanelController.getContainerHeight(), 0 /* animateDurationMs */); - mShouldAnimatePanel = false; - } - } - - protected void onErrorTimeoutFinish() {} - - protected void onCredentialVerified(@NonNull VerifyCredentialResponse response, int timeoutMs) { - if (response.isMatched()) { - mClearErrorRunnable.run(); - mLockPatternUtils.userPresent(mEffectiveUserId); - - // The response passed into this method contains the Gatekeeper Password. We still - // have to request Gatekeeper to create a Hardware Auth Token with the - // Gatekeeper Password and Challenge (keystore operationId in this case) - final long pwHandle = response.getGatekeeperPasswordHandle(); - final VerifyCredentialResponse gkResponse = mLockPatternUtils - .verifyGatekeeperPasswordHandle(pwHandle, mOperationId, mEffectiveUserId); - - mCallback.onCredentialMatched(gkResponse.getGatekeeperHAT()); - mLockPatternUtils.removeGatekeeperPasswordHandle(pwHandle); - } else { - if (timeoutMs > 0) { - mHandler.removeCallbacks(mClearErrorRunnable); - long deadline = mLockPatternUtils.setLockoutAttemptDeadline( - mEffectiveUserId, timeoutMs); - mErrorTimer = new ErrorTimer(mContext, - deadline - SystemClock.elapsedRealtime(), - LockPatternUtils.FAILED_ATTEMPT_COUNTDOWN_INTERVAL_MS, - mErrorView) { - @Override - public void onFinish() { - onErrorTimeoutFinish(); - mClearErrorRunnable.run(); - } - }; - mErrorTimer.start(); - } else { - final boolean didUpdateErrorText = reportFailedAttempt(); - if (!didUpdateErrorText) { - final @StringRes int errorRes; - switch (mCredentialType) { - case Utils.CREDENTIAL_PIN: - errorRes = R.string.biometric_dialog_wrong_pin; - break; - case Utils.CREDENTIAL_PATTERN: - errorRes = R.string.biometric_dialog_wrong_pattern; - break; - case Utils.CREDENTIAL_PASSWORD: - default: - errorRes = R.string.biometric_dialog_wrong_password; - break; - } - showError(getResources().getString(errorRes)); - } - } - } - } - - private boolean reportFailedAttempt() { - boolean result = updateErrorMessage( - mLockPatternUtils.getCurrentFailedPasswordAttempts(mEffectiveUserId) + 1); - mLockPatternUtils.reportFailedPasswordAttempt(mEffectiveUserId); - return result; - } - - private boolean updateErrorMessage(int numAttempts) { - // Don't show any message if there's no maximum number of attempts. - final int maxAttempts = mLockPatternUtils.getMaximumFailedPasswordsForWipe( - mEffectiveUserId); - if (maxAttempts <= 0 || numAttempts <= 0) { - return false; - } - - // Update the on-screen error string. - if (mErrorView != null) { - final String message = getResources().getString( - R.string.biometric_dialog_credential_attempts_before_wipe, - numAttempts, - maxAttempts); - showError(message); - } - - // Only show dialog if <=1 attempts are left before wiping. - final int remainingAttempts = maxAttempts - numAttempts; - if (remainingAttempts == 1) { - showLastAttemptBeforeWipeDialog(); - } else if (remainingAttempts <= 0) { - showNowWipingDialog(); - } - return true; - } - - private void showLastAttemptBeforeWipeDialog() { - mBackgroundExecutor.execute(() -> { - final AlertDialog alertDialog = new AlertDialog.Builder(mContext) - .setTitle(R.string.biometric_dialog_last_attempt_before_wipe_dialog_title) - .setMessage( - getLastAttemptBeforeWipeMessage(getUserTypeForWipe(), mCredentialType)) - .setPositiveButton(android.R.string.ok, null) - .create(); - alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL); - mHandler.post(alertDialog::show); - }); - } - - private void showNowWipingDialog() { - mBackgroundExecutor.execute(() -> { - String nowWipingMessage = getNowWipingMessage(getUserTypeForWipe()); - final AlertDialog alertDialog = new AlertDialog.Builder(mContext) - .setMessage(nowWipingMessage) - .setPositiveButton( - com.android.settingslib.R.string.failed_attempts_now_wiping_dialog_dismiss, - null /* OnClickListener */) - .setOnDismissListener( - dialog -> mContainerView.animateAway( - AuthDialogCallback.DISMISSED_ERROR)) - .create(); - alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL); - mHandler.post(alertDialog::show); - }); - } - - private @UserType int getUserTypeForWipe() { - final UserInfo userToBeWiped = mUserManager.getUserInfo( - mDevicePolicyManager.getProfileWithMinimumFailedPasswordsForWipe(mEffectiveUserId)); - if (userToBeWiped == null || userToBeWiped.isPrimary()) { - return USER_TYPE_PRIMARY; - } else if (userToBeWiped.isManagedProfile()) { - return USER_TYPE_MANAGED_PROFILE; - } else { - return USER_TYPE_SECONDARY; - } - } - - // This should not be called on the main thread to avoid making an IPC. - private String getLastAttemptBeforeWipeMessage( - @UserType int userType, @Utils.CredentialType int credentialType) { - switch (userType) { - case USER_TYPE_PRIMARY: - return getLastAttemptBeforeWipeDeviceMessage(credentialType); - case USER_TYPE_MANAGED_PROFILE: - return getLastAttemptBeforeWipeProfileMessage(credentialType); - case USER_TYPE_SECONDARY: - return getLastAttemptBeforeWipeUserMessage(credentialType); - default: - throw new IllegalArgumentException("Unrecognized user type:" + userType); - } - } - - private String getLastAttemptBeforeWipeDeviceMessage( - @Utils.CredentialType int credentialType) { - switch (credentialType) { - case Utils.CREDENTIAL_PIN: - return mContext.getString( - R.string.biometric_dialog_last_pin_attempt_before_wipe_device); - case Utils.CREDENTIAL_PATTERN: - return mContext.getString( - R.string.biometric_dialog_last_pattern_attempt_before_wipe_device); - case Utils.CREDENTIAL_PASSWORD: - default: - return mContext.getString( - R.string.biometric_dialog_last_password_attempt_before_wipe_device); - } - } - - // This should not be called on the main thread to avoid making an IPC. - private String getLastAttemptBeforeWipeProfileMessage( - @Utils.CredentialType int credentialType) { - return mDevicePolicyManager.getResources().getString( - getLastAttemptBeforeWipeProfileUpdatableStringId(credentialType), - () -> getLastAttemptBeforeWipeProfileDefaultMessage(credentialType)); - } - - private static String getLastAttemptBeforeWipeProfileUpdatableStringId( - @Utils.CredentialType int credentialType) { - switch (credentialType) { - case Utils.CREDENTIAL_PIN: - return BIOMETRIC_DIALOG_WORK_PIN_LAST_ATTEMPT; - case Utils.CREDENTIAL_PATTERN: - return BIOMETRIC_DIALOG_WORK_PATTERN_LAST_ATTEMPT; - case Utils.CREDENTIAL_PASSWORD: - default: - return BIOMETRIC_DIALOG_WORK_PASSWORD_LAST_ATTEMPT; - } - } - - private String getLastAttemptBeforeWipeProfileDefaultMessage( - @Utils.CredentialType int credentialType) { - int resId; - switch (credentialType) { - case Utils.CREDENTIAL_PIN: - resId = R.string.biometric_dialog_last_pin_attempt_before_wipe_profile; - break; - case Utils.CREDENTIAL_PATTERN: - resId = R.string.biometric_dialog_last_pattern_attempt_before_wipe_profile; - break; - case Utils.CREDENTIAL_PASSWORD: - default: - resId = R.string.biometric_dialog_last_password_attempt_before_wipe_profile; - } - return mContext.getString(resId); - } - - private String getLastAttemptBeforeWipeUserMessage( - @Utils.CredentialType int credentialType) { - int resId; - switch (credentialType) { - case Utils.CREDENTIAL_PIN: - resId = R.string.biometric_dialog_last_pin_attempt_before_wipe_user; - break; - case Utils.CREDENTIAL_PATTERN: - resId = R.string.biometric_dialog_last_pattern_attempt_before_wipe_user; - break; - case Utils.CREDENTIAL_PASSWORD: - default: - resId = R.string.biometric_dialog_last_password_attempt_before_wipe_user; - } - return mContext.getString(resId); - } - - private String getNowWipingMessage(@UserType int userType) { - return mDevicePolicyManager.getResources().getString( - getNowWipingUpdatableStringId(userType), - () -> getNowWipingDefaultMessage(userType)); - } - - private String getNowWipingUpdatableStringId(@UserType int userType) { - switch (userType) { - case USER_TYPE_MANAGED_PROFILE: - return BIOMETRIC_DIALOG_WORK_LOCK_FAILED_ATTEMPTS; - default: - return UNDEFINED; - } - } - - private String getNowWipingDefaultMessage(@UserType int userType) { - int resId; - switch (userType) { - case USER_TYPE_PRIMARY: - resId = com.android.settingslib.R.string.failed_attempts_now_wiping_device; - break; - case USER_TYPE_MANAGED_PROFILE: - resId = com.android.settingslib.R.string.failed_attempts_now_wiping_profile; - break; - case USER_TYPE_SECONDARY: - resId = com.android.settingslib.R.string.failed_attempts_now_wiping_user; - break; - default: - throw new IllegalArgumentException("Unrecognized user type:" + userType); - } - return mContext.getString(resId); - } - - @Nullable - private static CharSequence getTitle(@NonNull PromptInfo promptInfo) { - final CharSequence credentialTitle = promptInfo.getDeviceCredentialTitle(); - return credentialTitle != null ? credentialTitle : promptInfo.getTitle(); - } - - @Nullable - private static CharSequence getSubtitle(@NonNull PromptInfo promptInfo) { - final CharSequence credentialSubtitle = promptInfo.getDeviceCredentialSubtitle(); - return credentialSubtitle != null ? credentialSubtitle : promptInfo.getSubtitle(); - } - - @Nullable - private static CharSequence getDescription(@NonNull PromptInfo promptInfo) { - final CharSequence credentialDescription = promptInfo.getDeviceCredentialDescription(); - return credentialDescription != null ? credentialDescription : promptInfo.getDescription(); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java index f1e42e0c5454..5c616f005d4d 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java @@ -177,11 +177,11 @@ public class AuthPanelController extends ViewOutlineProvider { } } - int getContainerWidth() { + public int getContainerWidth() { return mContainerWidth; } - int getContainerHeight() { + public int getContainerHeight() { return mContainerHeight; } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java index 3273d7429d49..48c60a0548ad 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java @@ -60,6 +60,8 @@ import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.doze.DozeReceiver; import com.android.systemui.dump.DumpManager; +import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.flags.Flags; import com.android.systemui.keyguard.ScreenLifecycle; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; @@ -119,6 +121,7 @@ public class UdfpsController implements DozeReceiver { @NonNull private final SystemUIDialogManager mDialogManager; @NonNull private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; @NonNull private final VibratorHelper mVibrator; + @NonNull private final FeatureFlags mFeatureFlags; @NonNull private final FalsingManager mFalsingManager; @NonNull private final PowerManager mPowerManager; @NonNull private final AccessibilityManager mAccessibilityManager; @@ -202,6 +205,10 @@ public class UdfpsController implements DozeReceiver { @Override public void showUdfpsOverlay(long requestId, int sensorId, int reason, @NonNull IUdfpsOverlayControllerCallback callback) { + if (mFeatureFlags.isEnabled(Flags.NEW_UDFPS_OVERLAY)) { + return; + } + mFgExecutor.execute(() -> UdfpsController.this.showUdfpsOverlay( new UdfpsControllerOverlay(mContext, mFingerprintManager, mInflater, mWindowManager, mAccessibilityManager, mStatusBarStateController, @@ -217,6 +224,10 @@ public class UdfpsController implements DozeReceiver { @Override public void hideUdfpsOverlay(int sensorId) { + if (mFeatureFlags.isEnabled(Flags.NEW_UDFPS_OVERLAY)) { + return; + } + mFgExecutor.execute(() -> { if (mKeyguardUpdateMonitor.isFingerprintDetectionRunning()) { // if we get here, we expect keyguardUpdateMonitor's fingerprintRunningState @@ -590,6 +601,7 @@ public class UdfpsController implements DozeReceiver { @NonNull StatusBarKeyguardViewManager statusBarKeyguardViewManager, @NonNull DumpManager dumpManager, @NonNull KeyguardUpdateMonitor keyguardUpdateMonitor, + @NonNull FeatureFlags featureFlags, @NonNull FalsingManager falsingManager, @NonNull PowerManager powerManager, @NonNull AccessibilityManager accessibilityManager, @@ -625,6 +637,7 @@ public class UdfpsController implements DozeReceiver { mDumpManager = dumpManager; mDialogManager = dialogManager; mKeyguardUpdateMonitor = keyguardUpdateMonitor; + mFeatureFlags = featureFlags; mFalsingManager = falsingManager; mPowerManager = powerManager; mAccessibilityManager = accessibilityManager; @@ -859,9 +872,7 @@ public class UdfpsController implements DozeReceiver { playStartHaptic(); if (!mKeyguardUpdateMonitor.isFaceDetectionRunning()) { - mKeyguardUpdateMonitor.requestFaceAuth( - /* userInitiatedRequest */ false, - FaceAuthApiRequestReason.UDFPS_POINTER_DOWN); + mKeyguardUpdateMonitor.requestFaceAuth(FaceAuthApiRequestReason.UDFPS_POINTER_DOWN); } } mOnFingerDown = true; diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlay.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlay.kt index 6e78f3d3d6aa..142642a2411f 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlay.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlay.kt @@ -19,26 +19,40 @@ package com.android.systemui.biometrics import android.annotation.SuppressLint import android.content.Context import android.graphics.PixelFormat +import android.graphics.Point import android.graphics.Rect +import android.hardware.biometrics.BiometricOverlayConstants import android.hardware.fingerprint.FingerprintManager import android.hardware.fingerprint.FingerprintSensorPropertiesInternal import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback +import android.hardware.fingerprint.IUdfpsOverlay import android.os.Handler +import android.provider.Settings import android.view.MotionEvent -import android.view.View import android.view.WindowManager import android.view.WindowManager.LayoutParams.INPUT_FEATURE_SPY import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.CoreStartable 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.util.concurrency.DelayableExecutor import com.android.systemui.util.concurrency.Execution -import java.util.* +import java.util.Optional import java.util.concurrent.Executor import javax.inject.Inject +import kotlin.math.cos +import kotlin.math.pow +import kotlin.math.sin private const val TAG = "UdfpsOverlay" +const val SETTING_OVERLAY_DEBUG = "udfps_overlay_debug" + +// Number of sensor points needed inside ellipse for good overlap +private const val NEEDED_POINTS = 2 + @SuppressLint("ClickableViewAccessibility") @SysUISingleton class UdfpsOverlay @@ -51,10 +65,11 @@ constructor( private val handler: Handler, private val biometricExecutor: Executor, private val alternateTouchProvider: Optional<AlternateUdfpsTouchProvider>, - private val fgExecutor: DelayableExecutor, + @Main private val fgExecutor: DelayableExecutor, private val keyguardUpdateMonitor: KeyguardUpdateMonitor, private val authController: AuthController, - private val udfpsLogger: UdfpsLogger + private val udfpsLogger: UdfpsLogger, + private var featureFlags: FeatureFlags ) : CoreStartable { /** The view, when [isShowing], or null. */ @@ -64,7 +79,11 @@ constructor( private var requestId: Long = 0 private var onFingerDown = false val size = windowManager.maximumWindowMetrics.bounds + val udfpsProps: MutableList<FingerprintSensorPropertiesInternal> = mutableListOf() + var points: Array<Point> = emptyArray() + var processedMotionEvent = false + var isShowing = false private var params: UdfpsOverlayParams = UdfpsOverlayParams() @@ -87,41 +106,57 @@ constructor( inputFeatures = INPUT_FEATURE_SPY } - fun onTouch(v: View, event: MotionEvent): Boolean { - val view = v as UdfpsOverlayView + fun onTouch(event: MotionEvent): Boolean { + val view = overlayView!! return when (event.action) { MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> { onFingerDown = true if (!view.isDisplayConfigured && alternateTouchProvider.isPresent) { - biometricExecutor.execute { - alternateTouchProvider - .get() - .onPointerDown( - requestId, - event.x.toInt(), - event.y.toInt(), - event.touchMinor, - event.touchMajor - ) - } - fgExecutor.execute { - if (keyguardUpdateMonitor.isFingerprintDetectionRunning) { - keyguardUpdateMonitor.onUdfpsPointerDown(requestId.toInt()) + view.processMotionEvent(event) + + val goodOverlap = + if (featureFlags.isEnabled(Flags.NEW_ELLIPSE_DETECTION)) { + isGoodEllipseOverlap(event) + } else { + isGoodCentroidOverlap(event) } - } - view.configureDisplay { - biometricExecutor.execute { alternateTouchProvider.get().onUiReady() } + if (!processedMotionEvent && goodOverlap) { + biometricExecutor.execute { + alternateTouchProvider + .get() + .onPointerDown( + requestId, + event.rawX.toInt(), + event.rawY.toInt(), + event.touchMinor, + event.touchMajor + ) + } + fgExecutor.execute { + if (keyguardUpdateMonitor.isFingerprintDetectionRunning) { + keyguardUpdateMonitor.onUdfpsPointerDown(requestId.toInt()) + } + + view.configureDisplay { + biometricExecutor.execute { + alternateTouchProvider.get().onUiReady() + } + } + + processedMotionEvent = true + } } - } + view.invalidate() + } true } MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { - if (onFingerDown && alternateTouchProvider.isPresent) { + if (processedMotionEvent && alternateTouchProvider.isPresent) { biometricExecutor.execute { alternateTouchProvider.get().onPointerUp(requestId) } @@ -130,43 +165,105 @@ constructor( keyguardUpdateMonitor.onUdfpsPointerUp(requestId.toInt()) } } + + processedMotionEvent = false } - onFingerDown = false + if (view.isDisplayConfigured) { view.unconfigureDisplay() } + view.invalidate() true } else -> false } } - fun show(requestId: Long): Boolean { + fun isGoodEllipseOverlap(event: MotionEvent): Boolean { + return points.count { checkPoint(event, it) } >= NEEDED_POINTS + } + + fun isGoodCentroidOverlap(event: MotionEvent): Boolean { + return params.sensorBounds.contains(event.rawX.toInt(), event.rawY.toInt()) + } + + fun checkPoint(event: MotionEvent, point: Point): Boolean { + // Calculate if sensor point is within ellipse + // Formula: ((cos(o)(xE - xS) + sin(o)(yE - yS))^2 / a^2) + ((sin(o)(xE - xS) + cos(o)(yE - + // yS))^2 / b^2) <= 1 + val a: Float = cos(event.orientation) * (point.x - event.rawX) + val b: Float = sin(event.orientation) * (point.y - event.rawY) + val c: Float = sin(event.orientation) * (point.x - event.rawX) + val d: Float = cos(event.orientation) * (point.y - event.rawY) + val result = + (a + b).pow(2) / (event.touchMinor / 2).pow(2) + + (c - d).pow(2) / (event.touchMajor / 2).pow(2) + + return result <= 1 + } + + fun show(requestId: Long) { + if (!featureFlags.isEnabled(Flags.NEW_UDFPS_OVERLAY)) { + return + } + this.requestId = requestId - if (overlayView == null && alternateTouchProvider.isPresent) { - UdfpsOverlayView(context, null).let { - it.overlayParams = params - it.setUdfpsDisplayMode( - UdfpsDisplayMode(context, execution, authController, udfpsLogger) - ) - it.setOnTouchListener { v, event -> onTouch(v, event) } - overlayView = it + fgExecutor.execute { + if (overlayView == null && alternateTouchProvider.isPresent) { + UdfpsOverlayView(context, null).let { + it.overlayParams = params + it.setUdfpsDisplayMode( + UdfpsDisplayMode(context, execution, authController, udfpsLogger) + ) + it.setOnTouchListener { _, event -> onTouch(event) } + it.sensorPoints = points + it.debugOverlay = + Settings.Global.getInt( + context.contentResolver, + SETTING_OVERLAY_DEBUG, + 0 /* def */ + ) != 0 + overlayView = it + } + windowManager.addView(overlayView, coreLayoutParams) + isShowing = true } - windowManager.addView(overlayView, coreLayoutParams) - return true } - - return false } fun hide() { - overlayView?.apply { - windowManager.removeView(this) - setOnTouchListener(null) + if (!featureFlags.isEnabled(Flags.NEW_UDFPS_OVERLAY)) { + return } - overlayView = null + fgExecutor.execute { + if (overlayView != null && isShowing && alternateTouchProvider.isPresent) { + if (processedMotionEvent) { + biometricExecutor.execute { + alternateTouchProvider.get().onPointerUp(requestId) + } + fgExecutor.execute { + if (keyguardUpdateMonitor.isFingerprintDetectionRunning) { + keyguardUpdateMonitor.onUdfpsPointerUp(requestId.toInt()) + } + } + } + + if (overlayView!!.isDisplayConfigured) { + overlayView!!.unconfigureDisplay() + } + + overlayView?.apply { + windowManager.removeView(this) + setOnTouchListener(null) + } + + isShowing = false + overlayView = null + processedMotionEvent = false + } + } } @Override @@ -180,6 +277,18 @@ constructor( } } ) + + fingerprintManager?.setUdfpsOverlay( + object : IUdfpsOverlay.Stub() { + override fun show( + requestId: Long, + sensorId: Int, + @BiometricOverlayConstants.ShowReason reason: Int + ) = show(requestId) + + override fun hide(sensorId: Int) = hide() + } + ) } private fun handleAllFingerprintAuthenticatorsRegistered( @@ -201,6 +310,24 @@ constructor( naturalDisplayHeight = size.height(), scaleFactor = 1f ) + + val sensorX = params.sensorBounds.centerX() + val sensorY = params.sensorBounds.centerY() + val cornerOffset: Int = params.sensorBounds.width() / 4 + val sideOffset: Int = params.sensorBounds.width() / 3 + + points = + arrayOf( + Point(sensorX - cornerOffset, sensorY - cornerOffset), + Point(sensorX, sensorY - sideOffset), + Point(sensorX + cornerOffset, sensorY - cornerOffset), + Point(sensorX - sideOffset, sensorY), + Point(sensorX, sensorY), + Point(sensorX + sideOffset, sensorY), + Point(sensorX - cornerOffset, sensorY + cornerOffset), + Point(sensorX, sensorY + sideOffset), + Point(sensorX + cornerOffset, sensorY + cornerOffset) + ) } } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayView.kt index d37133239531..4e6a06b1c44b 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayView.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayView.kt @@ -20,26 +20,41 @@ import android.content.Context import android.graphics.Canvas import android.graphics.Color import android.graphics.Paint +import android.graphics.Point import android.graphics.RectF import android.util.AttributeSet +import android.view.MotionEvent import android.widget.FrameLayout private const val TAG = "UdfpsOverlayView" +private const val POINT_SIZE = 10f class UdfpsOverlayView(context: Context, attrs: AttributeSet?) : FrameLayout(context, attrs) { - - private val sensorRect = RectF() var overlayParams = UdfpsOverlayParams() private var mUdfpsDisplayMode: UdfpsDisplayMode? = null + var debugOverlay = false + var overlayPaint = Paint() var sensorPaint = Paint() + var touchPaint = Paint() + var pointPaint = Paint() val centerPaint = Paint() + var oval = RectF() + /** True after the call to [configureDisplay] and before the call to [unconfigureDisplay]. */ var isDisplayConfigured: Boolean = false private set + var touchX: Float = 0f + var touchY: Float = 0f + var touchMinor: Float = 0f + var touchMajor: Float = 0f + var touchOrientation: Double = 0.0 + + var sensorPoints: Array<Point>? = null + init { this.setWillNotDraw(false) } @@ -47,24 +62,60 @@ class UdfpsOverlayView(context: Context, attrs: AttributeSet?) : FrameLayout(con override fun onAttachedToWindow() { super.onAttachedToWindow() - overlayPaint.color = Color.argb(120, 255, 0, 0) + overlayPaint.color = Color.argb(100, 255, 0, 0) overlayPaint.style = Paint.Style.FILL + touchPaint.color = Color.argb(200, 255, 255, 255) + touchPaint.style = Paint.Style.FILL + sensorPaint.color = Color.argb(150, 134, 204, 255) sensorPaint.style = Paint.Style.FILL + + pointPaint.color = Color.WHITE + pointPaint.style = Paint.Style.FILL } override fun onDraw(canvas: Canvas) { super.onDraw(canvas) - canvas.drawRect(overlayParams.overlayBounds, overlayPaint) - canvas.drawRect(overlayParams.sensorBounds, sensorPaint) + if (debugOverlay) { + // Draw overlay and sensor bounds + canvas.drawRect(overlayParams.overlayBounds, overlayPaint) + canvas.drawRect(overlayParams.sensorBounds, sensorPaint) + } + + // Draw sensor circle canvas.drawCircle( overlayParams.sensorBounds.exactCenterX(), overlayParams.sensorBounds.exactCenterY(), overlayParams.sensorBounds.width().toFloat() / 2, centerPaint ) + + if (debugOverlay) { + // Draw Points + sensorPoints?.forEach { + canvas.drawCircle(it.x.toFloat(), it.y.toFloat(), POINT_SIZE, pointPaint) + } + + // Draw touch oval + canvas.save() + canvas.rotate(Math.toDegrees(touchOrientation).toFloat(), touchX, touchY) + + oval.setEmpty() + oval.set( + touchX - touchMinor / 2, + touchY + touchMajor / 2, + touchX + touchMinor / 2, + touchY - touchMajor / 2 + ) + + canvas.drawOval(oval, touchPaint) + + // Draw center point + canvas.drawCircle(touchX, touchY, POINT_SIZE, centerPaint) + canvas.restore() + } } fun setUdfpsDisplayMode(udfpsDisplayMode: UdfpsDisplayMode?) { @@ -80,4 +131,12 @@ class UdfpsOverlayView(context: Context, attrs: AttributeSet?) : FrameLayout(con isDisplayConfigured = false mUdfpsDisplayMode?.disable(null /* onDisabled */) } + + fun processMotionEvent(event: MotionEvent) { + touchX = event.rawX + touchY = event.rawY + touchMinor = event.touchMinor + touchMajor = event.touchMajor + touchOrientation = event.orientation.toDouble() + } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsShell.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsShell.kt index 75640b787a62..da50f1c94f29 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsShell.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsShell.kt @@ -62,7 +62,6 @@ class UdfpsShell @Inject constructor( if (args.size == 1 && args[0] == "hide") { hideOverlay() } else if (args.size == 2 && args[0] == "udfpsOverlay" && args[1] == "show") { - hideOverlay() showUdfpsOverlay() } else if (args.size == 2 && args[0] == "udfpsOverlay" && args[1] == "hide") { hideUdfpsOverlay() diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt b/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt index b5d81f253916..7c0c3b710e66 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt @@ -16,32 +16,45 @@ package com.android.systemui.biometrics.dagger +import com.android.systemui.biometrics.data.repository.PromptRepository +import com.android.systemui.biometrics.data.repository.PromptRepositoryImpl +import com.android.systemui.biometrics.domain.interactor.CredentialInteractor +import com.android.systemui.biometrics.domain.interactor.CredentialInteractorImpl import com.android.systemui.dagger.SysUISingleton import com.android.systemui.util.concurrency.ThreadFactory +import dagger.Binds import dagger.Module import dagger.Provides import java.util.concurrent.Executor import javax.inject.Qualifier -/** - * Dagger module for all things biometric. - */ +/** Dagger module for all things biometric. */ @Module -object BiometricsModule { +interface BiometricsModule { - /** Background [Executor] for HAL related operations. */ - @Provides + @Binds @SysUISingleton - @JvmStatic - @BiometricsBackground - fun providesPluginExecutor(threadFactory: ThreadFactory): Executor = - threadFactory.buildExecutorOnNewThread("biometrics") + fun biometricPromptRepository(impl: PromptRepositoryImpl): PromptRepository + + @Binds + @SysUISingleton + fun providesCredentialInteractor(impl: CredentialInteractorImpl): CredentialInteractor + + companion object { + /** Background [Executor] for HAL related operations. */ + @Provides + @SysUISingleton + @JvmStatic + @BiometricsBackground + fun providesPluginExecutor(threadFactory: ThreadFactory): Executor = + threadFactory.buildExecutorOnNewThread("biometrics") + } } /** - * Background executor for HAL operations that are latency sensitive but too - * slow to run on the main thread. Prefer the shared executors, such as - * [com.android.systemui.dagger.qualifiers.Background] when a HAL is not directly involved. + * Background executor for HAL operations that are latency sensitive but too slow to run on the main + * thread. Prefer the shared executors, such as [com.android.systemui.dagger.qualifiers.Background] + * when a HAL is not directly involved. */ @Qualifier @MustBeDocumented diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/model/PromptKind.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/model/PromptKind.kt new file mode 100644 index 000000000000..e82646f0d861 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/model/PromptKind.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.biometrics.data.model + +import com.android.systemui.biometrics.Utils + +// TODO(b/251476085): this should eventually replace Utils.CredentialType +/** Credential options for biometric prompt. Shadows [Utils.CredentialType]. */ +enum class PromptKind { + ANY_BIOMETRIC, + PIN, + PATTERN, + PASSWORD, +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt new file mode 100644 index 000000000000..92a13cfe538b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt @@ -0,0 +1,102 @@ +package com.android.systemui.biometrics.data.repository + +import android.hardware.biometrics.PromptInfo +import com.android.systemui.biometrics.AuthController +import com.android.systemui.biometrics.data.model.PromptKind +import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.dagger.SysUISingleton +import javax.inject.Inject +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +/** + * A repository for the global state of BiometricPrompt. + * + * There is never more than one instance of the prompt at any given time. + */ +interface PromptRepository { + + /** If the prompt is showing. */ + val isShowing: Flow<Boolean> + + /** The app-specific details to show in the prompt. */ + val promptInfo: StateFlow<PromptInfo?> + + /** The user that the prompt is for. */ + val userId: StateFlow<Int?> + + /** The gatekeeper challenge, if one is associated with this prompt. */ + val challenge: StateFlow<Long?> + + /** The kind of credential to use (biometric, pin, pattern, etc.). */ + val kind: StateFlow<PromptKind> + + /** Update the prompt configuration, which should be set before [isShowing]. */ + fun setPrompt( + promptInfo: PromptInfo, + userId: Int, + gatekeeperChallenge: Long?, + kind: PromptKind = PromptKind.ANY_BIOMETRIC, + ) + + /** Unset the prompt info. */ + fun unsetPrompt() +} + +@SysUISingleton +class PromptRepositoryImpl @Inject constructor(private val authController: AuthController) : + PromptRepository { + + override val isShowing: Flow<Boolean> = conflatedCallbackFlow { + val callback = + object : AuthController.Callback { + override fun onBiometricPromptShown() = + trySendWithFailureLogging(true, TAG, "set isShowing") + + override fun onBiometricPromptDismissed() = + trySendWithFailureLogging(false, TAG, "unset isShowing") + } + authController.addCallback(callback) + trySendWithFailureLogging(authController.isShowing, TAG, "update isShowing") + awaitClose { authController.removeCallback(callback) } + } + + private val _promptInfo: MutableStateFlow<PromptInfo?> = MutableStateFlow(null) + override val promptInfo = _promptInfo.asStateFlow() + + private val _challenge: MutableStateFlow<Long?> = MutableStateFlow(null) + override val challenge: StateFlow<Long?> = _challenge.asStateFlow() + + private val _userId: MutableStateFlow<Int?> = MutableStateFlow(null) + override val userId = _userId.asStateFlow() + + private val _kind: MutableStateFlow<PromptKind> = MutableStateFlow(PromptKind.ANY_BIOMETRIC) + override val kind = _kind.asStateFlow() + + override fun setPrompt( + promptInfo: PromptInfo, + userId: Int, + gatekeeperChallenge: Long?, + kind: PromptKind, + ) { + _kind.value = kind + _userId.value = userId + _challenge.value = gatekeeperChallenge + _promptInfo.value = promptInfo + } + + override fun unsetPrompt() { + _promptInfo.value = null + _userId.value = null + _challenge.value = null + _kind.value = PromptKind.ANY_BIOMETRIC + } + + companion object { + private const val TAG = "BiometricPromptRepository" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractor.kt new file mode 100644 index 000000000000..1f1a1b5c83bd --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractor.kt @@ -0,0 +1,282 @@ +package com.android.systemui.biometrics.domain.interactor + +import android.app.admin.DevicePolicyManager +import android.app.admin.DevicePolicyResources +import android.content.Context +import android.os.UserManager +import com.android.internal.widget.LockPatternUtils +import com.android.internal.widget.LockscreenCredential +import com.android.internal.widget.VerifyCredentialResponse +import com.android.systemui.R +import com.android.systemui.biometrics.domain.model.BiometricPromptRequest +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.util.time.SystemClock +import javax.inject.Inject +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow + +/** + * A wrapper for [LockPatternUtils] to verify PIN, pattern, or password credentials. + * + * This class also uses the [DevicePolicyManager] to generate appropriate error messages when policy + * exceptions are raised (i.e. wipe device due to excessive failed attempts, etc.). + */ +interface CredentialInteractor { + /** If the user's pattern credential should be hidden */ + fun isStealthModeActive(userId: Int): Boolean + + /** Get the effective user id (profile owner, if one exists) */ + fun getCredentialOwnerOrSelfId(userId: Int): Int + + /** + * Verifies a credential and returns a stream of results. + * + * The final emitted value will either be a [CredentialStatus.Fail.Error] or a + * [CredentialStatus.Success.Verified]. + */ + fun verifyCredential( + request: BiometricPromptRequest.Credential, + credential: LockscreenCredential, + ): Flow<CredentialStatus> +} + +/** Standard implementation of [CredentialInteractor]. */ +class CredentialInteractorImpl +@Inject +constructor( + @Application private val applicationContext: Context, + private val lockPatternUtils: LockPatternUtils, + private val userManager: UserManager, + private val devicePolicyManager: DevicePolicyManager, + private val systemClock: SystemClock, +) : CredentialInteractor { + + override fun isStealthModeActive(userId: Int): Boolean = + !lockPatternUtils.isVisiblePatternEnabled(userId) + + override fun getCredentialOwnerOrSelfId(userId: Int): Int = + userManager.getCredentialOwnerProfile(userId) + + override fun verifyCredential( + request: BiometricPromptRequest.Credential, + credential: LockscreenCredential, + ): Flow<CredentialStatus> = flow { + // Request LockSettingsService to return the Gatekeeper Password in the + // VerifyCredentialResponse so that we can request a Gatekeeper HAT with the + // Gatekeeper Password and operationId. + val effectiveUserId = request.userInfo.deviceCredentialOwnerId + val response = + lockPatternUtils.verifyCredential( + credential, + effectiveUserId, + LockPatternUtils.VERIFY_FLAG_REQUEST_GK_PW_HANDLE + ) + + if (response.isMatched) { + lockPatternUtils.userPresent(effectiveUserId) + + // The response passed into this method contains the Gatekeeper + // Password. We still have to request Gatekeeper to create a + // Hardware Auth Token with the Gatekeeper Password and Challenge + // (keystore operationId in this case) + val pwHandle = response.gatekeeperPasswordHandle + val gkResponse: VerifyCredentialResponse = + lockPatternUtils.verifyGatekeeperPasswordHandle( + pwHandle, + request.operationInfo.gatekeeperChallenge, + effectiveUserId + ) + val hat = gkResponse.gatekeeperHAT + lockPatternUtils.removeGatekeeperPasswordHandle(pwHandle) + emit(CredentialStatus.Success.Verified(hat)) + } else if (response.timeout > 0) { + // if requests are being throttled, update the error message every + // second until the temporary lock has expired + val deadline: Long = + lockPatternUtils.setLockoutAttemptDeadline(effectiveUserId, response.timeout) + val interval = LockPatternUtils.FAILED_ATTEMPT_COUNTDOWN_INTERVAL_MS + var remaining = deadline - systemClock.elapsedRealtime() + while (remaining > 0) { + emit( + CredentialStatus.Fail.Throttled( + applicationContext.getString( + R.string.biometric_dialog_credential_too_many_attempts, + remaining / 1000 + ) + ) + ) + delay(interval) + remaining -= interval + } + emit(CredentialStatus.Fail.Error("")) + } else { // bad request, but not throttled + val numAttempts = lockPatternUtils.getCurrentFailedPasswordAttempts(effectiveUserId) + 1 + val maxAttempts = lockPatternUtils.getMaximumFailedPasswordsForWipe(effectiveUserId) + if (maxAttempts <= 0 || numAttempts <= 0) { + // use a generic message if there's no maximum number of attempts + emit(CredentialStatus.Fail.Error()) + } else { + val remainingAttempts = (maxAttempts - numAttempts).coerceAtLeast(0) + emit( + CredentialStatus.Fail.Error( + applicationContext.getString( + R.string.biometric_dialog_credential_attempts_before_wipe, + numAttempts, + maxAttempts + ), + remainingAttempts, + fetchFinalAttemptMessageOrNull(request, remainingAttempts) + ) + ) + } + lockPatternUtils.reportFailedPasswordAttempt(effectiveUserId) + } + } + + private fun fetchFinalAttemptMessageOrNull( + request: BiometricPromptRequest.Credential, + remainingAttempts: Int?, + ): String? = + if (remainingAttempts != null && remainingAttempts <= 1) { + applicationContext.getFinalAttemptMessageOrBlank( + request, + devicePolicyManager, + userManager.getUserTypeForWipe( + devicePolicyManager, + request.userInfo.deviceCredentialOwnerId + ), + remainingAttempts + ) + } else { + null + } +} + +private enum class UserType { + PRIMARY, + MANAGED_PROFILE, + SECONDARY, +} + +private fun UserManager.getUserTypeForWipe( + devicePolicyManager: DevicePolicyManager, + effectiveUserId: Int, +): UserType { + val userToBeWiped = + getUserInfo( + devicePolicyManager.getProfileWithMinimumFailedPasswordsForWipe(effectiveUserId) + ) + return when { + userToBeWiped == null || userToBeWiped.isPrimary -> UserType.PRIMARY + userToBeWiped.isManagedProfile -> UserType.MANAGED_PROFILE + else -> UserType.SECONDARY + } +} + +private fun Context.getFinalAttemptMessageOrBlank( + request: BiometricPromptRequest.Credential, + devicePolicyManager: DevicePolicyManager, + userType: UserType, + remaining: Int, +): String = + when { + remaining == 1 -> getLastAttemptBeforeWipeMessage(request, devicePolicyManager, userType) + remaining <= 0 -> getNowWipingMessage(devicePolicyManager, userType) + else -> "" + } + +private fun Context.getLastAttemptBeforeWipeMessage( + request: BiometricPromptRequest.Credential, + devicePolicyManager: DevicePolicyManager, + userType: UserType, +): String = + when (userType) { + UserType.PRIMARY -> getLastAttemptBeforeWipeDeviceMessage(request) + UserType.MANAGED_PROFILE -> + getLastAttemptBeforeWipeProfileMessage(request, devicePolicyManager) + UserType.SECONDARY -> getLastAttemptBeforeWipeUserMessage(request) + } + +private fun Context.getLastAttemptBeforeWipeDeviceMessage( + request: BiometricPromptRequest.Credential, +): String { + val id = + when (request) { + is BiometricPromptRequest.Credential.Pin -> + R.string.biometric_dialog_last_pin_attempt_before_wipe_device + is BiometricPromptRequest.Credential.Pattern -> + R.string.biometric_dialog_last_pattern_attempt_before_wipe_device + is BiometricPromptRequest.Credential.Password -> + R.string.biometric_dialog_last_password_attempt_before_wipe_device + } + return getString(id) +} + +private fun Context.getLastAttemptBeforeWipeProfileMessage( + request: BiometricPromptRequest.Credential, + devicePolicyManager: DevicePolicyManager, +): String { + val id = + when (request) { + is BiometricPromptRequest.Credential.Pin -> + DevicePolicyResources.Strings.SystemUi.BIOMETRIC_DIALOG_WORK_PIN_LAST_ATTEMPT + is BiometricPromptRequest.Credential.Pattern -> + DevicePolicyResources.Strings.SystemUi.BIOMETRIC_DIALOG_WORK_PATTERN_LAST_ATTEMPT + is BiometricPromptRequest.Credential.Password -> + DevicePolicyResources.Strings.SystemUi.BIOMETRIC_DIALOG_WORK_PASSWORD_LAST_ATTEMPT + } + return devicePolicyManager.resources.getString(id) { + // use fallback a string if not found + val defaultId = + when (request) { + is BiometricPromptRequest.Credential.Pin -> + R.string.biometric_dialog_last_pin_attempt_before_wipe_profile + is BiometricPromptRequest.Credential.Pattern -> + R.string.biometric_dialog_last_pattern_attempt_before_wipe_profile + is BiometricPromptRequest.Credential.Password -> + R.string.biometric_dialog_last_password_attempt_before_wipe_profile + } + getString(defaultId) + } +} + +private fun Context.getLastAttemptBeforeWipeUserMessage( + request: BiometricPromptRequest.Credential, +): String { + val resId = + when (request) { + is BiometricPromptRequest.Credential.Pin -> + R.string.biometric_dialog_last_pin_attempt_before_wipe_user + is BiometricPromptRequest.Credential.Pattern -> + R.string.biometric_dialog_last_pattern_attempt_before_wipe_user + is BiometricPromptRequest.Credential.Password -> + R.string.biometric_dialog_last_password_attempt_before_wipe_user + } + return getString(resId) +} + +private fun Context.getNowWipingMessage( + devicePolicyManager: DevicePolicyManager, + userType: UserType, +): String { + val id = + when (userType) { + UserType.MANAGED_PROFILE -> + DevicePolicyResources.Strings.SystemUi.BIOMETRIC_DIALOG_WORK_LOCK_FAILED_ATTEMPTS + else -> DevicePolicyResources.UNDEFINED + } + return devicePolicyManager.resources.getString(id) { + // use fallback a string if not found + val defaultId = + when (userType) { + UserType.PRIMARY -> + com.android.settingslib.R.string.failed_attempts_now_wiping_device + UserType.MANAGED_PROFILE -> + com.android.settingslib.R.string.failed_attempts_now_wiping_profile + UserType.SECONDARY -> + com.android.settingslib.R.string.failed_attempts_now_wiping_user + } + getString(defaultId) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialStatus.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialStatus.kt new file mode 100644 index 000000000000..40b76121f237 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialStatus.kt @@ -0,0 +1,23 @@ +package com.android.systemui.biometrics.domain.interactor + +/** Result of a [CredentialInteractor.verifyCredential] check. */ +sealed interface CredentialStatus { + /** A successful result. */ + sealed interface Success : CredentialStatus { + /** The credential is valid and a [hat] has been generated. */ + data class Verified(val hat: ByteArray) : Success + } + /** A failed result. */ + sealed interface Fail : CredentialStatus { + val error: String? + + /** The credential check failed with an [error]. */ + data class Error( + override val error: String? = null, + val remainingAttempts: Int? = null, + val urgentMessage: String? = null, + ) : Fail + /** The credential check failed with an [error] and is temporarily locked out. */ + data class Throttled(override val error: String) : Fail + } +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractor.kt new file mode 100644 index 000000000000..6362c2f627d3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractor.kt @@ -0,0 +1,189 @@ +package com.android.systemui.biometrics.domain.interactor + +import android.hardware.biometrics.PromptInfo +import com.android.internal.widget.LockPatternView +import com.android.internal.widget.LockscreenCredential +import com.android.systemui.biometrics.Utils +import com.android.systemui.biometrics.data.model.PromptKind +import com.android.systemui.biometrics.data.repository.PromptRepository +import com.android.systemui.biometrics.domain.model.BiometricOperationInfo +import com.android.systemui.biometrics.domain.model.BiometricPromptRequest +import com.android.systemui.biometrics.domain.model.BiometricUserInfo +import com.android.systemui.dagger.qualifiers.Background +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.lastOrNull +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.withContext + +/** + * Business logic for BiometricPrompt's CredentialViews, which primarily includes checking a users + * PIN, pattern, or password credential instead of a biometric. + */ +class BiometricPromptCredentialInteractor +@Inject +constructor( + @Background private val bgDispatcher: CoroutineDispatcher, + private val biometricPromptRepository: PromptRepository, + private val credentialInteractor: CredentialInteractor, +) { + /** If the prompt is currently showing. */ + val isShowing: Flow<Boolean> = biometricPromptRepository.isShowing + + /** Metadata about the current credential prompt, including app-supplied preferences. */ + val prompt: Flow<BiometricPromptRequest?> = + combine( + biometricPromptRepository.promptInfo, + biometricPromptRepository.challenge, + biometricPromptRepository.userId, + biometricPromptRepository.kind + ) { promptInfo, challenge, userId, kind -> + if (promptInfo == null || userId == null || challenge == null) { + return@combine null + } + + when (kind) { + PromptKind.PIN -> + BiometricPromptRequest.Credential.Pin( + info = promptInfo, + userInfo = userInfo(userId), + operationInfo = operationInfo(challenge) + ) + PromptKind.PATTERN -> + BiometricPromptRequest.Credential.Pattern( + info = promptInfo, + userInfo = userInfo(userId), + operationInfo = operationInfo(challenge), + stealthMode = credentialInteractor.isStealthModeActive(userId) + ) + PromptKind.PASSWORD -> + BiometricPromptRequest.Credential.Password( + info = promptInfo, + userInfo = userInfo(userId), + operationInfo = operationInfo(challenge) + ) + else -> null + } + } + .distinctUntilChanged() + + private fun userInfo(userId: Int): BiometricUserInfo = + BiometricUserInfo( + userId = userId, + deviceCredentialOwnerId = credentialInteractor.getCredentialOwnerOrSelfId(userId) + ) + + private fun operationInfo(challenge: Long): BiometricOperationInfo = + BiometricOperationInfo(gatekeeperChallenge = challenge) + + /** Most recent error due to [verifyCredential]. */ + private val _verificationError = MutableStateFlow<CredentialStatus.Fail?>(null) + val verificationError: Flow<CredentialStatus.Fail?> = _verificationError.asStateFlow() + + /** Update the current request to use credential-based authentication instead of biometrics. */ + fun useCredentialsForAuthentication( + promptInfo: PromptInfo, + @Utils.CredentialType kind: Int, + userId: Int, + challenge: Long, + ) { + biometricPromptRepository.setPrompt( + promptInfo, + userId, + challenge, + kind.asBiometricPromptCredential() + ) + } + + /** Unset the current authentication request. */ + fun resetPrompt() { + biometricPromptRepository.unsetPrompt() + } + + /** + * Check a credential and return the attestation token (HAT) if successful. + * + * This method will not return if credential checks are being throttled until the throttling has + * expired and the user can try again. It will periodically update the [verificationError] until + * cancelled or the throttling has completed. If the request is not throttled, but unsuccessful, + * the [verificationError] will be set and an optional + * [CredentialStatus.Fail.Error.urgentMessage] message may be provided to indicate additional + * hints to the user (i.e. device will be wiped on next failure, etc.). + * + * The check happens on the background dispatcher given in the constructor. + */ + suspend fun checkCredential( + request: BiometricPromptRequest.Credential, + text: CharSequence? = null, + pattern: List<LockPatternView.Cell>? = null, + ): CredentialStatus = + withContext(bgDispatcher) { + val credential = + when (request) { + is BiometricPromptRequest.Credential.Pin -> + LockscreenCredential.createPinOrNone(text ?: "") + is BiometricPromptRequest.Credential.Password -> + LockscreenCredential.createPasswordOrNone(text ?: "") + is BiometricPromptRequest.Credential.Pattern -> + LockscreenCredential.createPattern(pattern ?: listOf()) + } + + credential.use { c -> verifyCredential(request, c) } + } + + private suspend fun verifyCredential( + request: BiometricPromptRequest.Credential, + credential: LockscreenCredential? + ): CredentialStatus { + if (credential == null || credential.isNone) { + return CredentialStatus.Fail.Error() + } + + val finalStatus = + credentialInteractor + .verifyCredential(request, credential) + .onEach { status -> + when (status) { + is CredentialStatus.Success -> _verificationError.value = null + is CredentialStatus.Fail -> _verificationError.value = status + } + } + .lastOrNull() + + return finalStatus ?: CredentialStatus.Fail.Error() + } + + /** + * Report a user-visible error. + * + * Use this instead of calling [verifyCredential] when it is not necessary because the check + * will obviously fail (i.e. too short, empty, etc.) + */ + fun setVerificationError(error: CredentialStatus.Fail.Error?) { + if (error != null) { + _verificationError.value = error + } else { + resetVerificationError() + } + } + + /** Clear the current error message, if any. */ + fun resetVerificationError() { + _verificationError.value = null + } +} + +// TODO(b/251476085): remove along with Utils.CredentialType +/** Convert a [Utils.CredentialType] to the corresponding [PromptKind]. */ +private fun @receiver:Utils.CredentialType Int.asBiometricPromptCredential(): PromptKind = + when (this) { + Utils.CREDENTIAL_PIN -> PromptKind.PIN + Utils.CREDENTIAL_PASSWORD -> PromptKind.PASSWORD + Utils.CREDENTIAL_PATTERN -> PromptKind.PATTERN + else -> PromptKind.ANY_BIOMETRIC + } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricOperationInfo.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricOperationInfo.kt new file mode 100644 index 000000000000..c619b12361c4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricOperationInfo.kt @@ -0,0 +1,4 @@ +package com.android.systemui.biometrics.domain.model + +/** Metadata about an in-progress biometric operation. */ +data class BiometricOperationInfo(val gatekeeperChallenge: Long = -1) diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt new file mode 100644 index 000000000000..5ee0381db630 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt @@ -0,0 +1,69 @@ +package com.android.systemui.biometrics.domain.model + +import android.hardware.biometrics.PromptInfo + +/** + * Preferences for BiometricPrompt, such as title & description, that are immutable while the prompt + * is showing. + * + * This roughly corresponds to a "request" by the system or an app to show BiometricPrompt and it + * contains a subset of the information in a [PromptInfo] that is relevant to SysUI. + */ +sealed class BiometricPromptRequest( + val title: String, + val subtitle: String, + val description: String, + val userInfo: BiometricUserInfo, + val operationInfo: BiometricOperationInfo, +) { + /** Prompt using one or more biometrics. */ + class Biometric( + info: PromptInfo, + userInfo: BiometricUserInfo, + operationInfo: BiometricOperationInfo, + ) : + BiometricPromptRequest( + title = info.title?.toString() ?: "", + subtitle = info.subtitle?.toString() ?: "", + description = info.description?.toString() ?: "", + userInfo = userInfo, + operationInfo = operationInfo + ) + + /** Prompt using a credential (pin, pattern, password). */ + sealed class Credential( + info: PromptInfo, + userInfo: BiometricUserInfo, + operationInfo: BiometricOperationInfo, + ) : + BiometricPromptRequest( + title = (info.deviceCredentialTitle ?: info.title)?.toString() ?: "", + subtitle = (info.deviceCredentialSubtitle ?: info.subtitle)?.toString() ?: "", + description = (info.deviceCredentialDescription ?: info.description)?.toString() ?: "", + userInfo = userInfo, + operationInfo = operationInfo, + ) { + + /** PIN prompt. */ + class Pin( + info: PromptInfo, + userInfo: BiometricUserInfo, + operationInfo: BiometricOperationInfo, + ) : Credential(info, userInfo, operationInfo) + + /** Password prompt. */ + class Password( + info: PromptInfo, + userInfo: BiometricUserInfo, + operationInfo: BiometricOperationInfo, + ) : Credential(info, userInfo, operationInfo) + + /** Pattern prompt. */ + class Pattern( + info: PromptInfo, + userInfo: BiometricUserInfo, + operationInfo: BiometricOperationInfo, + val stealthMode: Boolean, + ) : Credential(info, userInfo, operationInfo) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricUserInfo.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricUserInfo.kt new file mode 100644 index 000000000000..08da04d27606 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricUserInfo.kt @@ -0,0 +1,7 @@ +package com.android.systemui.biometrics.domain.model + +/** Metadata about the current user BiometricPrompt is being shown to. */ +data class BiometricUserInfo( + val userId: Int, + val deviceCredentialOwnerId: Int = userId, +) diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPasswordView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPasswordView.kt new file mode 100644 index 000000000000..bcc0575651e4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPasswordView.kt @@ -0,0 +1,130 @@ +package com.android.systemui.biometrics.ui + +import android.content.Context +import android.content.res.Configuration.ORIENTATION_LANDSCAPE +import android.text.TextUtils +import android.util.AttributeSet +import android.view.View +import android.view.WindowInsets +import android.view.WindowInsets.Type.ime +import android.view.accessibility.AccessibilityManager +import android.widget.ImageView +import android.widget.ImeAwareEditText +import android.widget.LinearLayout +import android.widget.TextView +import androidx.core.view.isGone +import com.android.systemui.R +import com.android.systemui.biometrics.AuthPanelController +import com.android.systemui.biometrics.ui.binder.CredentialViewBinder +import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel + +/** PIN or password credential view for BiometricPrompt. */ +class CredentialPasswordView(context: Context, attrs: AttributeSet?) : + LinearLayout(context, attrs), CredentialView, View.OnApplyWindowInsetsListener { + + private lateinit var titleView: TextView + private lateinit var subtitleView: TextView + private lateinit var descriptionView: TextView + private lateinit var iconView: ImageView + private lateinit var passwordField: ImeAwareEditText + private lateinit var credentialHeader: View + private lateinit var credentialInput: View + + private var bottomInset: Int = 0 + + private val accessibilityManager by lazy { + context.getSystemService(AccessibilityManager::class.java) + } + + /** Initializes the view. */ + override fun init( + viewModel: CredentialViewModel, + host: CredentialView.Host, + panelViewController: AuthPanelController, + animatePanel: Boolean, + ) { + CredentialViewBinder.bind(this, host, viewModel, panelViewController, animatePanel) + } + + override fun onFinishInflate() { + super.onFinishInflate() + + titleView = requireViewById(R.id.title) + subtitleView = requireViewById(R.id.subtitle) + descriptionView = requireViewById(R.id.description) + iconView = requireViewById(R.id.icon) + subtitleView = requireViewById(R.id.subtitle) + passwordField = requireViewById(R.id.lockPassword) + credentialHeader = requireViewById(R.id.auth_credential_header) + credentialInput = requireViewById(R.id.auth_credential_input) + + setOnApplyWindowInsetsListener(this) + } + + override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) { + super.onLayout(changed, left, top, right, bottom) + + val inputLeftBound: Int + val inputTopBound: Int + var headerRightBound = right + var headerTopBounds = top + val subTitleBottom: Int = if (subtitleView.isGone) titleView.bottom else subtitleView.bottom + val descBottom = if (descriptionView.isGone) subTitleBottom else descriptionView.bottom + if (resources.configuration.orientation == ORIENTATION_LANDSCAPE) { + inputTopBound = (bottom - credentialInput.height) / 2 + inputLeftBound = (right - left) / 2 + headerRightBound = inputLeftBound + headerTopBounds -= iconView.bottom.coerceAtMost(bottomInset) + } else { + inputTopBound = descBottom + (bottom - descBottom - credentialInput.height) / 2 + inputLeftBound = (right - left - credentialInput.width) / 2 + } + + if (descriptionView.bottom > bottomInset) { + credentialHeader.layout(left, headerTopBounds, headerRightBound, bottom) + } + credentialInput.layout(inputLeftBound, inputTopBound, right, bottom) + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + + val newWidth = MeasureSpec.getSize(widthMeasureSpec) + val newHeight = MeasureSpec.getSize(heightMeasureSpec) - bottomInset + + setMeasuredDimension(newWidth, newHeight) + + val halfWidthSpec = MeasureSpec.makeMeasureSpec(width / 2, MeasureSpec.AT_MOST) + val fullHeightSpec = MeasureSpec.makeMeasureSpec(newHeight, MeasureSpec.UNSPECIFIED) + if (resources.configuration.orientation == ORIENTATION_LANDSCAPE) { + measureChildren(halfWidthSpec, fullHeightSpec) + } else { + measureChildren(widthMeasureSpec, fullHeightSpec) + } + } + + override fun onApplyWindowInsets(v: View, insets: WindowInsets): WindowInsets { + val bottomInsets = insets.getInsets(ime()) + if (bottomInset != bottomInsets.bottom) { + bottomInset = bottomInsets.bottom + + if (bottomInset > 0 && resources.configuration.orientation == ORIENTATION_LANDSCAPE) { + titleView.isSingleLine = true + titleView.ellipsize = TextUtils.TruncateAt.MARQUEE + titleView.marqueeRepeatLimit = -1 + // select to enable marquee unless a screen reader is enabled + titleView.isSelected = accessibilityManager.shouldMarquee() + } else { + titleView.isSingleLine = false + titleView.ellipsize = null + // select to enable marquee unless a screen reader is enabled + titleView.isSelected = false + } + + requestLayout() + } + return insets + } +} + +private fun AccessibilityManager.shouldMarquee(): Boolean = !isEnabled || !isTouchExplorationEnabled diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPatternView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPatternView.kt new file mode 100644 index 000000000000..75331f083851 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPatternView.kt @@ -0,0 +1,23 @@ +package com.android.systemui.biometrics.ui + +import android.content.Context +import android.util.AttributeSet +import android.widget.LinearLayout +import com.android.systemui.biometrics.AuthPanelController +import com.android.systemui.biometrics.ui.binder.CredentialViewBinder +import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel + +/** Pattern credential view for BiometricPrompt. */ +class CredentialPatternView(context: Context, attrs: AttributeSet?) : + LinearLayout(context, attrs), CredentialView { + + /** Initializes the view. */ + override fun init( + viewModel: CredentialViewModel, + host: CredentialView.Host, + panelViewController: AuthPanelController, + animatePanel: Boolean, + ) { + CredentialViewBinder.bind(this, host, viewModel, panelViewController, animatePanel) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialView.kt new file mode 100644 index 000000000000..b7c6a4566108 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialView.kt @@ -0,0 +1,31 @@ +package com.android.systemui.biometrics.ui + +import com.android.systemui.biometrics.AuthPanelController +import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel + +/** A credential variant of BiometricPrompt. */ +sealed interface CredentialView { + /** + * Callbacks for the "host" container view that contains this credential view. + * + * TODO(b/251476085): Removed when the host view is converted to use a parent view model. + */ + interface Host { + /** When the user's credential has been verified. */ + fun onCredentialMatched(attestation: ByteArray) + + /** When the user abandons credential verification. */ + fun onCredentialAborted() + + /** Warn the user is warned about excessive attempts. */ + fun onCredentialAttemptsRemaining(remaining: Int, messageBody: String) + } + + // TODO(251476085): remove AuthPanelController + fun init( + viewModel: CredentialViewModel, + host: Host, + panelViewController: AuthPanelController, + animatePanel: Boolean, + ) +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialPasswordViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialPasswordViewBinder.kt new file mode 100644 index 000000000000..c619648a314c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialPasswordViewBinder.kt @@ -0,0 +1,104 @@ +package com.android.systemui.biometrics.ui.binder + +import android.view.KeyEvent +import android.view.View +import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InputMethodManager +import android.widget.ImeAwareEditText +import android.widget.TextView +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle +import com.android.systemui.R +import com.android.systemui.biometrics.ui.CredentialPasswordView +import com.android.systemui.biometrics.ui.CredentialView +import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel +import com.android.systemui.lifecycle.repeatWhenAttached +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch + +/** Sub-binder for the [CredentialPasswordView]. */ +object CredentialPasswordViewBinder { + + /** Bind the view. */ + fun bind( + view: CredentialPasswordView, + host: CredentialView.Host, + viewModel: CredentialViewModel, + ) { + val imeManager = view.context.getSystemService(InputMethodManager::class.java)!! + + val passwordField: ImeAwareEditText = view.requireViewById(R.id.lockPassword) + + view.repeatWhenAttached { + passwordField.requestFocus() + passwordField.scheduleShowSoftInput() + + repeatOnLifecycle(Lifecycle.State.STARTED) { + // observe credential validation attempts and submit/cancel buttons + launch { + viewModel.header.collect { header -> + passwordField.setTextOperationUser(header.user) + passwordField.setOnEditorActionListener( + OnImeSubmitListener { text -> + launch { viewModel.checkCredential(text, header) } + } + ) + passwordField.setOnKeyListener( + OnBackButtonListener { host.onCredentialAborted() } + ) + } + } + + launch { + viewModel.inputFlags.collect { flags -> + flags?.let { passwordField.inputType = it } + } + } + + // dismiss on a valid credential check + launch { + viewModel.validatedAttestation.collect { attestation -> + if (attestation != null) { + imeManager.hideSoftInputFromWindow(view.windowToken, 0 /* flags */) + host.onCredentialMatched(attestation) + } else { + passwordField.setText("") + } + } + } + } + } + } +} + +private class OnBackButtonListener(private val onBack: () -> Unit) : View.OnKeyListener { + override fun onKey(v: View, keyCode: Int, event: KeyEvent): Boolean { + if (keyCode != KeyEvent.KEYCODE_BACK) { + return false + } + if (event.action == KeyEvent.ACTION_UP) { + onBack() + } + return true + } +} + +private class OnImeSubmitListener(private val onSubmit: (text: CharSequence) -> Unit) : + TextView.OnEditorActionListener { + override fun onEditorAction(v: TextView, actionId: Int, event: KeyEvent?): Boolean { + val isSoftImeEvent = + event == null && + (actionId == EditorInfo.IME_NULL || + actionId == EditorInfo.IME_ACTION_DONE || + actionId == EditorInfo.IME_ACTION_NEXT) + val isKeyboardEnterKey = + event != null && + KeyEvent.isConfirmKey(event.keyCode) && + event.action == KeyEvent.ACTION_DOWN + if (isSoftImeEvent || isKeyboardEnterKey) { + onSubmit(v.text) + return true + } + return false + } +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialPatternViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialPatternViewBinder.kt new file mode 100644 index 000000000000..4765551df3f0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialPatternViewBinder.kt @@ -0,0 +1,75 @@ +package com.android.systemui.biometrics.ui.binder + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle +import com.android.internal.widget.LockPatternUtils +import com.android.internal.widget.LockPatternView +import com.android.systemui.R +import com.android.systemui.biometrics.ui.CredentialPatternView +import com.android.systemui.biometrics.ui.CredentialView +import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel +import com.android.systemui.lifecycle.repeatWhenAttached +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch + +/** Sub-binder for the [CredentialPatternView]. */ +object CredentialPatternViewBinder { + + /** Bind the view. */ + fun bind( + view: CredentialPatternView, + host: CredentialView.Host, + viewModel: CredentialViewModel, + ) { + val lockPatternView: LockPatternView = view.requireViewById(R.id.lockPattern) + + view.repeatWhenAttached { + repeatOnLifecycle(Lifecycle.State.STARTED) { + // observe credential validation attempts and submit/cancel buttons + launch { + viewModel.header.collect { header -> + lockPatternView.setOnPatternListener( + OnPatternDetectedListener { pattern -> + if (pattern.isPatternLongEnough()) { + // Pattern size is less than the minimum + // do not count it as a failed attempt + viewModel.showPatternTooShortError() + } else { + lockPatternView.isEnabled = false + launch { viewModel.checkCredential(pattern, header) } + } + } + ) + } + } + + launch { viewModel.stealthMode.collect { lockPatternView.isInStealthMode = it } } + + // dismiss on a valid credential check + launch { + viewModel.validatedAttestation.collect { attestation -> + val matched = attestation != null + lockPatternView.isEnabled = !matched + if (matched) { + host.onCredentialMatched(attestation!!) + } + } + } + } + } + } +} + +private class OnPatternDetectedListener( + private val onDetected: (pattern: List<LockPatternView.Cell>) -> Unit +) : LockPatternView.OnPatternListener { + override fun onPatternCellAdded(pattern: List<LockPatternView.Cell>) {} + override fun onPatternCleared() {} + override fun onPatternStart() {} + override fun onPatternDetected(pattern: List<LockPatternView.Cell>) { + onDetected(pattern) + } +} + +private fun List<LockPatternView.Cell>.isPatternLongEnough(): Boolean = + size < LockPatternUtils.MIN_PATTERN_REGISTER_FAIL diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt new file mode 100644 index 000000000000..fcc948756972 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt @@ -0,0 +1,140 @@ +package com.android.systemui.biometrics.ui.binder + +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle +import com.android.systemui.R +import com.android.systemui.animation.Interpolators +import com.android.systemui.biometrics.AuthDialog +import com.android.systemui.biometrics.AuthPanelController +import com.android.systemui.biometrics.ui.CredentialPasswordView +import com.android.systemui.biometrics.ui.CredentialPatternView +import com.android.systemui.biometrics.ui.CredentialView +import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel +import com.android.systemui.lifecycle.repeatWhenAttached +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch + +/** + * View binder for all credential variants of BiometricPrompt, including [CredentialPatternView] and + * [CredentialPasswordView]. + * + * This binder delegates to sub-binders for each variant, such as the [CredentialPasswordViewBinder] + * and [CredentialPatternViewBinder]. + */ +object CredentialViewBinder { + + /** Binds a [CredentialPasswordView] or [CredentialPatternView] to a [CredentialViewModel]. */ + @JvmStatic + fun bind( + view: ViewGroup, + host: CredentialView.Host, + viewModel: CredentialViewModel, + panelViewController: AuthPanelController, + animatePanel: Boolean, + maxErrorDuration: Long = 3_000L, + ) { + val titleView: TextView = view.requireViewById(R.id.title) + val subtitleView: TextView = view.requireViewById(R.id.subtitle) + val descriptionView: TextView = view.requireViewById(R.id.description) + val iconView: ImageView? = view.findViewById(R.id.icon) + val errorView: TextView = view.requireViewById(R.id.error) + + var errorTimer: Job? = null + + // bind common elements + view.repeatWhenAttached { + if (animatePanel) { + with(panelViewController) { + // Credential view is always full screen. + setUseFullScreen(true) + updateForContentDimensions( + containerWidth, + containerHeight, + 0 /* animateDurationMs */ + ) + } + } + + repeatOnLifecycle(Lifecycle.State.STARTED) { + // show prompt metadata + launch { + viewModel.header.collect { header -> + titleView.text = header.title + view.announceForAccessibility(header.title) + + subtitleView.textOrHide = header.subtitle + descriptionView.textOrHide = header.description + + iconView?.setImageDrawable(header.icon) + + // Only animate this if we're transitioning from a biometric view. + if (viewModel.animateContents.value) { + view.animateCredentialViewIn() + } + } + } + + // show transient error messages + launch { + viewModel.errorMessage + .onEach { msg -> + errorTimer?.cancel() + if (msg.isNotBlank()) { + errorTimer = launch { + delay(maxErrorDuration) + viewModel.resetErrorMessage() + } + } + } + .collect { errorView.textOrHide = it } + } + + // show an extra dialog if the remaining attempts becomes low + launch { + viewModel.remainingAttempts + .filter { it.remaining != null } + .collect { info -> + host.onCredentialAttemptsRemaining(info.remaining!!, info.message) + } + } + } + } + + // bind the auth widget + when (view) { + is CredentialPasswordView -> CredentialPasswordViewBinder.bind(view, host, viewModel) + is CredentialPatternView -> CredentialPatternViewBinder.bind(view, host, viewModel) + else -> throw IllegalStateException("unexpected view type: ${view.javaClass.name}") + } + } +} + +private fun View.animateCredentialViewIn() { + translationY = resources.getDimension(R.dimen.biometric_dialog_credential_translation_offset) + alpha = 0f + postOnAnimation { + animate() + .translationY(0f) + .setDuration(AuthDialog.ANIMATE_CREDENTIAL_INITIAL_DURATION_MS.toLong()) + .alpha(1f) + .setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN) + .withLayer() + .start() + } +} + +private var TextView.textOrHide: String? + set(value) { + val gone = value.isNullOrBlank() + visibility = if (gone) View.GONE else View.VISIBLE + text = if (gone) "" else value + } + get() = text?.toString() diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModel.kt new file mode 100644 index 000000000000..84bbceb38fa7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModel.kt @@ -0,0 +1,178 @@ +package com.android.systemui.biometrics.ui.viewmodel + +import android.content.Context +import android.graphics.drawable.Drawable +import android.os.UserHandle +import android.text.InputType +import com.android.internal.widget.LockPatternView +import com.android.systemui.R +import com.android.systemui.biometrics.Utils +import com.android.systemui.biometrics.domain.interactor.BiometricPromptCredentialInteractor +import com.android.systemui.biometrics.domain.interactor.CredentialStatus +import com.android.systemui.biometrics.domain.model.BiometricPromptRequest +import com.android.systemui.dagger.qualifiers.Application +import javax.inject.Inject +import kotlin.reflect.KClass +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.map + +/** View-model for all CredentialViews within BiometricPrompt. */ +class CredentialViewModel +@Inject +constructor( + @Application private val applicationContext: Context, + private val credentialInteractor: BiometricPromptCredentialInteractor, +) { + + /** Top level information about the prompt. */ + val header: Flow<HeaderViewModel> = + credentialInteractor.prompt.filterIsInstance<BiometricPromptRequest.Credential>().map { + request -> + BiometricPromptHeaderViewModelImpl( + request, + user = UserHandle.of(request.userInfo.userId), + title = request.title, + subtitle = request.subtitle, + description = request.description, + icon = applicationContext.asLockIcon(request.userInfo.deviceCredentialOwnerId), + ) + } + + /** Input flags for text based credential views */ + val inputFlags: Flow<Int?> = + credentialInteractor.prompt.map { + when (it) { + is BiometricPromptRequest.Credential.Pin -> + InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_PASSWORD + else -> null + } + } + + /** If stealth mode is active (hide user credential input). */ + val stealthMode: Flow<Boolean> = + credentialInteractor.prompt.map { + when (it) { + is BiometricPromptRequest.Credential.Pattern -> it.stealthMode + else -> false + } + } + + private val _animateContents: MutableStateFlow<Boolean> = MutableStateFlow(true) + /** If this view should be animated on transitions. */ + val animateContents = _animateContents.asStateFlow() + + /** Error messages to show the user. */ + val errorMessage: Flow<String> = + combine(credentialInteractor.verificationError, credentialInteractor.prompt) { error, p -> + when (error) { + is CredentialStatus.Fail.Error -> error.error + ?: applicationContext.asBadCredentialErrorMessage(p) + is CredentialStatus.Fail.Throttled -> error.error + null -> "" + } + } + + private val _validatedAttestation: MutableSharedFlow<ByteArray?> = MutableSharedFlow() + /** Results of [checkPatternCredential]. A non-null attestation is supplied on success. */ + val validatedAttestation: Flow<ByteArray?> = _validatedAttestation.asSharedFlow() + + private val _remainingAttempts: MutableStateFlow<RemainingAttempts> = + MutableStateFlow(RemainingAttempts()) + /** If set, the number of remaining attempts before the user must stop. */ + val remainingAttempts: Flow<RemainingAttempts> = _remainingAttempts.asStateFlow() + + /** Enable transition animations. */ + fun setAnimateContents(animate: Boolean) { + _animateContents.value = animate + } + + /** Show an error message to inform the user the pattern is too short to attempt validation. */ + fun showPatternTooShortError() { + credentialInteractor.setVerificationError( + CredentialStatus.Fail.Error( + applicationContext.asBadCredentialErrorMessage( + BiometricPromptRequest.Credential.Pattern::class + ) + ) + ) + } + + /** Reset the error message to an empty string. */ + fun resetErrorMessage() { + credentialInteractor.resetVerificationError() + } + + /** Check a PIN or password and update [validatedAttestation] or [remainingAttempts]. */ + suspend fun checkCredential(text: CharSequence, header: HeaderViewModel) = + checkCredential(credentialInteractor.checkCredential(header.asRequest(), text = text)) + + /** Check a pattern and update [validatedAttestation] or [remainingAttempts]. */ + suspend fun checkCredential(pattern: List<LockPatternView.Cell>, header: HeaderViewModel) = + checkCredential(credentialInteractor.checkCredential(header.asRequest(), pattern = pattern)) + + private suspend fun checkCredential(result: CredentialStatus) { + when (result) { + is CredentialStatus.Success.Verified -> { + _validatedAttestation.emit(result.hat) + _remainingAttempts.value = RemainingAttempts() + } + is CredentialStatus.Fail.Error -> { + _validatedAttestation.emit(null) + _remainingAttempts.value = + RemainingAttempts(result.remainingAttempts, result.urgentMessage ?: "") + } + is CredentialStatus.Fail.Throttled -> { + // required for completeness, but a throttled error cannot be the final result + _validatedAttestation.emit(null) + _remainingAttempts.value = RemainingAttempts() + } + } + } +} + +private fun Context.asBadCredentialErrorMessage(prompt: BiometricPromptRequest?): String = + asBadCredentialErrorMessage( + if (prompt != null) prompt::class else BiometricPromptRequest.Credential.Password::class + ) + +private fun <T : BiometricPromptRequest> Context.asBadCredentialErrorMessage( + clazz: KClass<T> +): String = + getString( + when (clazz) { + BiometricPromptRequest.Credential.Pin::class -> R.string.biometric_dialog_wrong_pin + BiometricPromptRequest.Credential.Password::class -> + R.string.biometric_dialog_wrong_password + BiometricPromptRequest.Credential.Pattern::class -> + R.string.biometric_dialog_wrong_pattern + else -> R.string.biometric_dialog_wrong_password + } + ) + +private fun Context.asLockIcon(userId: Int): Drawable { + val id = + if (Utils.isManagedProfile(this, userId)) { + R.drawable.auth_dialog_enterprise + } else { + R.drawable.auth_dialog_lock + } + return resources.getDrawable(id, theme) +} + +private class BiometricPromptHeaderViewModelImpl( + val request: BiometricPromptRequest.Credential, + override val user: UserHandle, + override val title: String, + override val subtitle: String, + override val description: String, + override val icon: Drawable, +) : HeaderViewModel + +private fun HeaderViewModel.asRequest(): BiometricPromptRequest.Credential = + (this as BiometricPromptHeaderViewModelImpl).request diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/HeaderViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/HeaderViewModel.kt new file mode 100644 index 000000000000..ba23f1cfa22d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/HeaderViewModel.kt @@ -0,0 +1,13 @@ +package com.android.systemui.biometrics.ui.viewmodel + +import android.graphics.drawable.Drawable +import android.os.UserHandle + +/** View model for the top-level header / info area of BiometricPrompt. */ +interface HeaderViewModel { + val user: UserHandle + val title: String + val subtitle: String + val description: String + val icon: Drawable +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/RemainingAttempts.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/RemainingAttempts.kt new file mode 100644 index 000000000000..0f221734cb44 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/RemainingAttempts.kt @@ -0,0 +1,4 @@ +package com.android.systemui.biometrics.ui.viewmodel + +/** Metadata about the number of credential attempts the user has left [remaining], if known. */ +data class RemainingAttempts(val remaining: Int? = null, val message: String = "") diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index 7e31626983e7..6db562107357 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -48,6 +48,7 @@ import com.android.systemui.log.dagger.LogModule; import com.android.systemui.mediaprojection.appselector.MediaProjectionModule; import com.android.systemui.model.SysUiState; import com.android.systemui.navigationbar.NavigationBarComponent; +import com.android.systemui.notetask.NoteTaskModule; import com.android.systemui.people.PeopleModule; import com.android.systemui.plugins.BcSmartspaceDataPlugin; import com.android.systemui.privacy.PrivacyModule; @@ -83,6 +84,7 @@ import com.android.systemui.statusbar.policy.dagger.SmartRepliesInflationModule; import com.android.systemui.statusbar.policy.dagger.StatusBarPolicyModule; import com.android.systemui.statusbar.window.StatusBarWindowModule; import com.android.systemui.telephony.data.repository.TelephonyRepositoryModule; +import com.android.systemui.temporarydisplay.dagger.TemporaryDisplayModule; import com.android.systemui.tuner.dagger.TunerModule; import com.android.systemui.unfold.SysUIUnfoldModule; import com.android.systemui.user.UserModule; @@ -149,9 +151,11 @@ import dagger.Provides; SysUIConcurrencyModule.class, SysUIUnfoldModule.class, TelephonyRepositoryModule.class, + TemporaryDisplayModule.class, TunerModule.class, UserModule.class, UtilModule.class, + NoteTaskModule.class, WalletModule.class }, subcomponents = { diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/Complication.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/Complication.java index 29bb2f42cca5..41f557850f88 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/complication/Complication.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/Complication.java @@ -164,7 +164,8 @@ public interface Complication { COMPLICATION_TYPE_AIR_QUALITY, COMPLICATION_TYPE_CAST_INFO, COMPLICATION_TYPE_HOME_CONTROLS, - COMPLICATION_TYPE_SMARTSPACE + COMPLICATION_TYPE_SMARTSPACE, + COMPLICATION_TYPE_MEDIA_ENTRY }) @Retention(RetentionPolicy.SOURCE) @interface ComplicationType {} @@ -177,6 +178,7 @@ public interface Complication { int COMPLICATION_TYPE_CAST_INFO = 1 << 4; int COMPLICATION_TYPE_HOME_CONTROLS = 1 << 5; int COMPLICATION_TYPE_SMARTSPACE = 1 << 6; + int COMPLICATION_TYPE_MEDIA_ENTRY = 1 << 7; /** * The {@link Host} interface specifies a way a {@link Complication} to communicate with its diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationUtils.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationUtils.java index 75a97de10e7e..18aacd21bd12 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationUtils.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationUtils.java @@ -20,6 +20,7 @@ import static com.android.systemui.dreams.complication.Complication.COMPLICATION import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_CAST_INFO; import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_DATE; import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_HOME_CONTROLS; +import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_MEDIA_ENTRY; import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_NONE; import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_SMARTSPACE; import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_TIME; @@ -54,6 +55,8 @@ public class ComplicationUtils { return COMPLICATION_TYPE_HOME_CONTROLS; case DreamBackend.COMPLICATION_TYPE_SMARTSPACE: return COMPLICATION_TYPE_SMARTSPACE; + case DreamBackend.COMPLICATION_TYPE_MEDIA_ENTRY: + return COMPLICATION_TYPE_MEDIA_ENTRY; default: return COMPLICATION_TYPE_NONE; } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamMediaEntryComplication.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamMediaEntryComplication.java index 1166c2fc1120..deff0608bedd 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamMediaEntryComplication.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamMediaEntryComplication.java @@ -55,6 +55,11 @@ public class DreamMediaEntryComplication implements Complication { return mComponentFactory.create().getViewHolder(); } + @Override + public int getRequiredTypeAvailability() { + return COMPLICATION_TYPE_MEDIA_ENTRY; + } + /** * Contains values/logic associated with the dream complication view. */ diff --git a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebug.java b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebug.java index 3adeeac2e4d4..1f061e9ff1c6 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebug.java +++ b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebug.java @@ -21,6 +21,7 @@ import static com.android.systemui.flags.FlagManager.ACTION_SET_FLAG; import static com.android.systemui.flags.FlagManager.EXTRA_FLAGS; import static com.android.systemui.flags.FlagManager.EXTRA_ID; import static com.android.systemui.flags.FlagManager.EXTRA_VALUE; +import static com.android.systemui.flags.FlagsCommonModule.ALL_FLAGS; import static java.util.Objects.requireNonNull; @@ -59,20 +60,20 @@ import javax.inject.Named; * * Flags can be set (or unset) via the following adb command: * - * adb shell cmd statusbar flag <id> <on|off|toggle|erase> + * adb shell cmd statusbar flag <id> <on|off|toggle|erase> * - * Alternatively, you can change flags via a broadcast intent: + * Alternatively, you can change flags via a broadcast intent: * - * adb shell am broadcast -a com.android.systemui.action.SET_FLAG --ei id <id> [--ez value <0|1>] + * adb shell am broadcast -a com.android.systemui.action.SET_FLAG --ei id <id> [--ez value <0|1>] * * To restore a flag back to its default, leave the `--ez value <0|1>` off of the command. */ @SysUISingleton public class FeatureFlagsDebug implements FeatureFlags { static final String TAG = "SysUIFlags"; - static final String ALL_FLAGS = "all_flags"; private final FlagManager mFlagManager; + private final Context mContext; private final SecureSettings mSecureSettings; private final Resources mResources; private final SystemPropertiesHelper mSystemProperties; @@ -83,6 +84,14 @@ public class FeatureFlagsDebug implements FeatureFlags { private final Map<Integer, String> mStringFlagCache = new TreeMap<>(); private final Restarter mRestarter; + private final ServerFlagReader.ChangeListener mOnPropertiesChanged = + new ServerFlagReader.ChangeListener() { + @Override + public void onChange() { + mRestarter.restart(); + } + }; + @Inject public FeatureFlagsDebug( FlagManager flagManager, @@ -93,23 +102,28 @@ public class FeatureFlagsDebug implements FeatureFlags { DeviceConfigProxy deviceConfigProxy, ServerFlagReader serverFlagReader, @Named(ALL_FLAGS) Map<Integer, Flag<?>> allFlags, - Restarter barService) { + Restarter restarter) { mFlagManager = flagManager; + mContext = context; mSecureSettings = secureSettings; mResources = resources; mSystemProperties = systemProperties; mDeviceConfigProxy = deviceConfigProxy; mServerFlagReader = serverFlagReader; mAllFlags = allFlags; - mRestarter = barService; + mRestarter = restarter; + } + /** Call after construction to setup listeners. */ + void init() { IntentFilter filter = new IntentFilter(); filter.addAction(ACTION_SET_FLAG); filter.addAction(ACTION_GET_FLAGS); - flagManager.setOnSettingsChangedAction(this::restartSystemUI); - flagManager.setClearCacheAction(this::removeFromCache); - context.registerReceiver(mReceiver, filter, null, null, + mFlagManager.setOnSettingsChangedAction(this::restartSystemUI); + mFlagManager.setClearCacheAction(this::removeFromCache); + mContext.registerReceiver(mReceiver, filter, null, null, Context.RECEIVER_EXPORTED_UNAUDITED); + mServerFlagReader.listenForChanges(mAllFlags.values(), mOnPropertiesChanged); } @Override @@ -196,7 +210,7 @@ public class FeatureFlagsDebug implements FeatureFlags { return mStringFlagCache.get(id); } - /** Specific override for Boolean flags that checks against the teamfood list.*/ + /** Specific override for Boolean flags that checks against the teamfood list. */ private boolean readFlagValue(int id, boolean defaultValue) { Boolean result = readBooleanFlagOverride(id); boolean hasServerOverride = mServerFlagReader.hasOverride(id); @@ -273,6 +287,7 @@ public class FeatureFlagsDebug implements FeatureFlags { private void dispatchListenersAndMaybeRestart(int id, Consumer<Boolean> restartAction) { mFlagManager.dispatchListenersAndMaybeRestart(id, restartAction); } + /** Works just like {@link #eraseFlag(int)} except that it doesn't restart SystemUI. */ private void eraseInternal(int id) { // We can't actually "erase" things from sysprops, but we can set them to empty! @@ -358,7 +373,7 @@ public class FeatureFlagsDebug implements FeatureFlags { } } - Bundle extras = getResultExtras(true); + Bundle extras = getResultExtras(true); if (extras != null) { extras.putParcelableArrayList(EXTRA_FLAGS, pFlags); } diff --git a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebugStartable.kt b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebugStartable.kt index 560dcbd78c42..62713348c789 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebugStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebugStartable.kt @@ -31,7 +31,7 @@ constructor( dumpManager: DumpManager, private val commandRegistry: CommandRegistry, private val flagCommand: FlagCommand, - featureFlags: FeatureFlags + private val featureFlags: FeatureFlagsDebug ) : CoreStartable { init { @@ -41,6 +41,7 @@ constructor( } override fun start() { + featureFlags.init() commandRegistry.registerCommand(FlagCommand.FLAG_COMMAND) { flagCommand } } } diff --git a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsRelease.java b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsRelease.java index 40a8a1a9ef01..30cad5faa2de 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsRelease.java +++ b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsRelease.java @@ -16,6 +16,8 @@ package com.android.systemui.flags; +import static com.android.systemui.flags.FlagsCommonModule.ALL_FLAGS; + import static java.util.Objects.requireNonNull; import android.content.res.Resources; @@ -34,6 +36,7 @@ import java.io.PrintWriter; import java.util.Map; import javax.inject.Inject; +import javax.inject.Named; /** * Default implementation of the a Flag manager that returns default values for release builds @@ -49,26 +52,47 @@ public class FeatureFlagsRelease implements FeatureFlags { private final SystemPropertiesHelper mSystemProperties; private final DeviceConfigProxy mDeviceConfigProxy; private final ServerFlagReader mServerFlagReader; + private final Restarter mRestarter; + private final Map<Integer, Flag<?>> mAllFlags; SparseBooleanArray mBooleanCache = new SparseBooleanArray(); SparseArray<String> mStringCache = new SparseArray<>(); + private final ServerFlagReader.ChangeListener mOnPropertiesChanged = + new ServerFlagReader.ChangeListener() { + @Override + public void onChange() { + mRestarter.restart(); + } + }; + @Inject public FeatureFlagsRelease( @Main Resources resources, SystemPropertiesHelper systemProperties, DeviceConfigProxy deviceConfigProxy, - ServerFlagReader serverFlagReader) { + ServerFlagReader serverFlagReader, + @Named(ALL_FLAGS) Map<Integer, Flag<?>> allFlags, + Restarter restarter) { mResources = resources; mSystemProperties = systemProperties; mDeviceConfigProxy = deviceConfigProxy; mServerFlagReader = serverFlagReader; + mAllFlags = allFlags; + mRestarter = restarter; + } + + /** Call after construction to setup listeners. */ + void init() { + mServerFlagReader.listenForChanges(mAllFlags.values(), mOnPropertiesChanged); } @Override - public void addListener(@NonNull Flag<?> flag, @NonNull Listener listener) {} + public void addListener(@NonNull Flag<?> flag, @NonNull Listener listener) { + } @Override - public void removeListener(@NonNull Listener listener) {} + public void removeListener(@NonNull Listener listener) { + } @Override public boolean isEnabled(@NotNull UnreleasedFlag flag) { diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagCommand.java b/packages/SystemUI/src/com/android/systemui/flags/FlagCommand.java index 4d254313a57b..1e93c0b7002d 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/FlagCommand.java +++ b/packages/SystemUI/src/com/android/systemui/flags/FlagCommand.java @@ -16,6 +16,8 @@ package com.android.systemui.flags; +import static com.android.systemui.flags.FlagsCommonModule.ALL_FLAGS; + import androidx.annotation.NonNull; import com.android.systemui.statusbar.commandline.Command; @@ -42,7 +44,7 @@ public class FlagCommand implements Command { @Inject FlagCommand( FeatureFlagsDebug featureFlags, - @Named(FeatureFlagsDebug.ALL_FLAGS) Map<Integer, Flag<?>> allFlags + @Named(ALL_FLAGS) Map<Integer, Flag<?>> allFlags ) { mFeatureFlags = featureFlags; mAllFlags = allFlags; diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index 3c116799db5c..a4d3399eeb5d 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 The Android Open Source Project + * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -57,13 +57,13 @@ object Flags { val INSTANT_VOICE_REPLY = UnreleasedFlag(111, teamfood = true) // TODO(b/254512425): Tracking Bug - val NOTIFICATION_MEMORY_MONITOR_ENABLED = UnreleasedFlag(112, teamfood = false) + val NOTIFICATION_MEMORY_MONITOR_ENABLED = UnreleasedFlag(112, teamfood = true) // TODO(b/254512731): Tracking Bug @JvmField val NOTIFICATION_DISMISSAL_FADE = UnreleasedFlag(113, teamfood = true) val STABILITY_INDEX_FIX = UnreleasedFlag(114, teamfood = true) val SEMI_STABLE_SORT = UnreleasedFlag(115, teamfood = true) - @JvmField val NOTIFICATION_GROUP_CORNER = UnreleasedFlag(116, true) + @JvmField val NOTIFICATION_GROUP_CORNER = UnreleasedFlag(116, teamfood = true) // next id: 117 // 200 - keyguard/lockscreen @@ -118,6 +118,16 @@ object Flags { */ @JvmField val STEP_CLOCK_ANIMATION = UnreleasedFlag(212) + /** + * Migration from the legacy isDozing/dozeAmount paths to the new KeyguardTransitionRepository + * will occur in stages. This is one stage of many to come. + */ + @JvmField val DOZING_MIGRATION_1 = UnreleasedFlag(213, teamfood = true) + + @JvmField val NEW_ELLIPSE_DETECTION = UnreleasedFlag(214) + + @JvmField val NEW_UDFPS_OVERLAY = UnreleasedFlag(215) + // 300 - power menu // TODO(b/254512600): Tracking Bug @JvmField val POWER_MENU_LITE = ReleasedFlag(300) @@ -132,7 +142,7 @@ object Flags { @Deprecated("Not needed anymore") val NEW_USER_SWITCHER = ReleasedFlag(500) // TODO(b/254512321): Tracking Bug - @JvmField val COMBINED_QS_HEADERS = ReleasedFlag(501, teamfood = true) + @JvmField val COMBINED_QS_HEADERS = UnreleasedFlag(501, teamfood = true) val PEOPLE_TILE = ResourceBooleanFlag(502, R.bool.flag_conversations) @JvmField val QS_USER_DETAIL_SHORTCUT = @@ -142,7 +152,7 @@ object Flags { @Deprecated("Not needed anymore") val NEW_FOOTER = ReleasedFlag(504) // TODO(b/254512747): Tracking Bug - val NEW_HEADER = ReleasedFlag(505, teamfood = true) + val NEW_HEADER = UnreleasedFlag(505, teamfood = true) // TODO(b/254512383): Tracking Bug @JvmField @@ -159,9 +169,6 @@ object Flags { // TODO(b/254513246): Tracking Bug val STATUS_BAR_USER_SWITCHER = ResourceBooleanFlag(602, R.bool.flag_user_switcher_chip) - // TODO(b/254513025): Tracking Bug - val STATUS_BAR_LETTERBOX_APPEARANCE = ReleasedFlag(603, teamfood = false) - // TODO(b/254512623): Tracking Bug @Deprecated("Replaced by mobile and wifi specific flags.") val NEW_STATUS_BAR_PIPELINE_BACKEND = UnreleasedFlag(604, teamfood = false) @@ -170,9 +177,9 @@ object Flags { @Deprecated("Replaced by mobile and wifi specific flags.") val NEW_STATUS_BAR_PIPELINE_FRONTEND = UnreleasedFlag(605, teamfood = false) - val NEW_STATUS_BAR_MOBILE_ICONS = UnreleasedFlag(606, false) + val NEW_STATUS_BAR_MOBILE_ICONS = UnreleasedFlag(606) - val NEW_STATUS_BAR_WIFI_ICON = UnreleasedFlag(607, false) + val NEW_STATUS_BAR_WIFI_ICON = UnreleasedFlag(607) // 700 - dialer/calls // TODO(b/254512734): Tracking Bug @@ -193,7 +200,7 @@ object Flags { // 802 - wallpaper rendering // TODO(b/254512923): Tracking Bug - @JvmField val USE_CANVAS_RENDERER = ReleasedFlag(802) + @JvmField val USE_CANVAS_RENDERER = UnreleasedFlag(802, teamfood = true) // 803 - screen contents translation // TODO(b/254513187): Tracking Bug @@ -227,15 +234,9 @@ object Flags { // 1000 - dock val SIMULATE_DOCK_THROUGH_CHARGING = ReleasedFlag(1000) - // TODO(b/254512444): Tracking Bug - @JvmField val DOCK_SETUP_ENABLED = ReleasedFlag(1001) - // TODO(b/254512758): Tracking Bug @JvmField val ROUNDED_BOX_RIPPLE = ReleasedFlag(1002) - // TODO(b/254512525): Tracking Bug - @JvmField val REFACTORED_DOCK_SETUP = ReleasedFlag(1003, teamfood = true) - // 1100 - windowing @Keep val WM_ENABLE_SHELL_TRANSITIONS = @@ -317,7 +318,7 @@ object Flags { // 1500 - chooser // TODO(b/254512507): Tracking Bug - val CHOOSER_UNBUNDLED = UnreleasedFlag(1500) + val CHOOSER_UNBUNDLED = UnreleasedFlag(1500, teamfood = true) // 1600 - accessibility @JvmField val A11Y_FLOATING_MENU_FLING_SPRING_ANIMATIONS = UnreleasedFlag(1600) @@ -326,7 +327,10 @@ object Flags { @JvmField val CLIPBOARD_OVERLAY_REFACTOR = UnreleasedFlag(1700) // 1800 - shade container - @JvmField val LEAVE_SHADE_OPEN_FOR_BUGREPORT = UnreleasedFlag(1800, true) + @JvmField val LEAVE_SHADE_OPEN_FOR_BUGREPORT = UnreleasedFlag(1800, teamfood = true) + + // 1900 - note task + @JvmField val NOTE_TASKS = SysPropBooleanFlag(1900, "persist.sysui.debug.note_tasks") // Pay no attention to the reflection behind the curtain. // ========================== Curtain ========================== diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagsCommonModule.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagsCommonModule.kt new file mode 100644 index 000000000000..e1f4944741a0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/flags/FlagsCommonModule.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.flags + +import com.android.internal.statusbar.IStatusBarService +import dagger.Module +import dagger.Provides +import javax.inject.Named + +/** Module containing shared code for all FeatureFlag implementations. */ +@Module +interface FlagsCommonModule { + companion object { + const val ALL_FLAGS = "all_flags" + + @JvmStatic + @Provides + @Named(ALL_FLAGS) + fun providesAllFlags(): Map<Int, Flag<*>> { + return Flags.collectFlags() + } + + @JvmStatic + @Provides + fun providesRestarter(barService: IStatusBarService): Restarter { + return object : Restarter { + override fun restart() { + barService.restart() + } + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/flags/ServerFlagReader.kt b/packages/SystemUI/src/com/android/systemui/flags/ServerFlagReader.kt index fc5b9f4eea05..694fa01bb4a5 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/ServerFlagReader.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/ServerFlagReader.kt @@ -16,9 +16,13 @@ package com.android.systemui.flags +import android.provider.DeviceConfig +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.util.DeviceConfigProxy -import dagger.Binds import dagger.Module +import dagger.Provides +import java.util.concurrent.Executor import javax.inject.Inject interface ServerFlagReader { @@ -27,40 +31,99 @@ interface ServerFlagReader { /** Returns any stored server-side setting or the default if not set. */ fun readServerOverride(flagId: Int, default: Boolean): Boolean + + /** Register a listener for changes to any of the passed in flags. */ + fun listenForChanges(values: Collection<Flag<*>>, listener: ChangeListener) + + interface ChangeListener { + fun onChange() + } } class ServerFlagReaderImpl @Inject constructor( - private val deviceConfig: DeviceConfigProxy + private val namespace: String, + private val deviceConfig: DeviceConfigProxy, + @Background private val executor: Executor ) : ServerFlagReader { + + private val listeners = + mutableListOf<Pair<ServerFlagReader.ChangeListener, Collection<Flag<*>>>>() + + private val onPropertiesChangedListener = object : DeviceConfig.OnPropertiesChangedListener { + override fun onPropertiesChanged(properties: DeviceConfig.Properties) { + if (properties.namespace != namespace) { + return + } + + for ((listener, flags) in listeners) { + propLoop@ for (propName in properties.keyset) { + for (flag in flags) { + if (propName == getServerOverrideName(flag.id)) { + listener.onChange() + break@propLoop + } + } + } + } + } + } + override fun hasOverride(flagId: Int): Boolean = deviceConfig.getProperty( - SYSUI_NAMESPACE, + namespace, getServerOverrideName(flagId) ) != null override fun readServerOverride(flagId: Int, default: Boolean): Boolean { return deviceConfig.getBoolean( - SYSUI_NAMESPACE, + namespace, getServerOverrideName(flagId), default ) } + override fun listenForChanges( + flags: Collection<Flag<*>>, + listener: ServerFlagReader.ChangeListener + ) { + if (listeners.isEmpty()) { + deviceConfig.addOnPropertiesChangedListener( + namespace, + executor, + onPropertiesChangedListener + ) + } + listeners.add(Pair(listener, flags)) + } + private fun getServerOverrideName(flagId: Int): String { return "flag_override_$flagId" } } -private val SYSUI_NAMESPACE = "systemui" - @Module interface ServerFlagReaderModule { - @Binds - fun bindsReader(impl: ServerFlagReaderImpl): ServerFlagReader + companion object { + private val SYSUI_NAMESPACE = "systemui" + + @JvmStatic + @Provides + @SysUISingleton + fun bindsReader( + deviceConfig: DeviceConfigProxy, + @Background executor: Executor + ): ServerFlagReader { + return ServerFlagReaderImpl( + SYSUI_NAMESPACE, deviceConfig, executor + ) + } + } } class ServerFlagReaderFake : ServerFlagReader { private val flagMap: MutableMap<Int, Boolean> = mutableMapOf() + private val listeners = + mutableListOf<Pair<ServerFlagReader.ChangeListener, Collection<Flag<*>>>>() override fun hasOverride(flagId: Int): Boolean { return flagMap.containsKey(flagId) @@ -72,9 +135,24 @@ class ServerFlagReaderFake : ServerFlagReader { fun setFlagValue(flagId: Int, value: Boolean) { flagMap.put(flagId, value) + + for ((listener, flags) in listeners) { + flagLoop@ for (flag in flags) { + if (flagId == flag.id) { + listener.onChange() + break@flagLoop + } + } + } } fun eraseFlag(flagId: Int) { flagMap.remove(flagId) } + + override fun listenForChanges( + flags: Collection<Flag<*>>, + listener: ServerFlagReader.ChangeListener + ) { + } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java index 1c6cec22ffca..021431399ab6 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java @@ -518,7 +518,7 @@ public class KeyguardService extends Service { @PowerManager.WakeReason int pmWakeReason, boolean cameraGestureTriggered) { Trace.beginSection("KeyguardService.mBinder#onStartedWakingUp"); checkPermission(); - mKeyguardViewMediator.onStartedWakingUp(cameraGestureTriggered); + mKeyguardViewMediator.onStartedWakingUp(pmWakeReason, cameraGestureTriggered); mKeyguardLifecyclesDispatcher.dispatch( KeyguardLifecyclesDispatcher.STARTED_WAKING_UP, pmWakeReason); Trace.endSection(); diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index 480fd93fc761..41abb62f05cc 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -342,12 +342,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, */ private int mDelayedProfileShowingSequence; - /** - * If the user has disabled the keyguard, then requests to exit, this is - * how we'll ultimately let them know whether it was successful. We use this - * var being non-null as an indicator that there is an in progress request. - */ - private IKeyguardExitCallback mExitSecureCallback; private final DismissCallbackRegistry mDismissCallbackRegistry; // the properties of the keyguard @@ -1342,18 +1336,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, || !mLockPatternUtils.isSecure(currentUser); long timeout = getLockTimeout(KeyguardUpdateMonitor.getCurrentUser()); mLockLater = false; - if (mExitSecureCallback != null) { - if (DEBUG) Log.d(TAG, "pending exit secure callback cancelled"); - try { - mExitSecureCallback.onKeyguardExitResult(false); - } catch (RemoteException e) { - Slog.w(TAG, "Failed to call onKeyguardExitResult(false)", e); - } - mExitSecureCallback = null; - if (!mExternallyEnabled) { - hideLocked(); - } - } else if (mShowing && !mKeyguardStateController.isKeyguardGoingAway()) { + if (mShowing && !mKeyguardStateController.isKeyguardGoingAway()) { // If we are going to sleep but the keyguard is showing (and will continue to be // showing, not in the process of going away) then reset its state. Otherwise, let // this fall through and explicitly re-lock the keyguard. @@ -1595,7 +1578,8 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, /** * It will let us know when the device is waking up. */ - public void onStartedWakingUp(boolean cameraGestureTriggered) { + public void onStartedWakingUp(@PowerManager.WakeReason int pmWakeReason, + boolean cameraGestureTriggered) { Trace.beginSection("KeyguardViewMediator#onStartedWakingUp"); // TODO: Rename all screen off/on references to interactive/sleeping @@ -1610,7 +1594,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, if (DEBUG) Log.d(TAG, "onStartedWakingUp, seq = " + mDelayedShowingSequence); notifyStartedWakingUp(); } - mUpdateMonitor.dispatchStartedWakingUp(); + mUpdateMonitor.dispatchStartedWakingUp(pmWakeReason); maybeSendUserPresentBroadcast(); Trace.endSection(); } @@ -1672,13 +1656,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, mExternallyEnabled = enabled; if (!enabled && mShowing) { - if (mExitSecureCallback != null) { - if (DEBUG) Log.d(TAG, "in process of verifyUnlock request, ignoring"); - // we're in the process of handling a request to verify the user - // can get past the keyguard. ignore extraneous requests to disable / re-enable - return; - } - // hiding keyguard that is showing, remember to reshow later if (DEBUG) Log.d(TAG, "remembering to reshow, hiding keyguard, " + "disabling status bar expansion"); @@ -1692,33 +1669,23 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, mNeedToReshowWhenReenabled = false; updateInputRestrictedLocked(); - if (mExitSecureCallback != null) { - if (DEBUG) Log.d(TAG, "onKeyguardExitResult(false), resetting"); + showLocked(null); + + // block until we know the keyguard is done drawing (and post a message + // to unblock us after a timeout, so we don't risk blocking too long + // and causing an ANR). + mWaitingUntilKeyguardVisible = true; + mHandler.sendEmptyMessageDelayed(KEYGUARD_DONE_DRAWING, + KEYGUARD_DONE_DRAWING_TIMEOUT_MS); + if (DEBUG) Log.d(TAG, "waiting until mWaitingUntilKeyguardVisible is false"); + while (mWaitingUntilKeyguardVisible) { try { - mExitSecureCallback.onKeyguardExitResult(false); - } catch (RemoteException e) { - Slog.w(TAG, "Failed to call onKeyguardExitResult(false)", e); + wait(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); } - mExitSecureCallback = null; - resetStateLocked(); - } else { - showLocked(null); - - // block until we know the keyguard is done drawing (and post a message - // to unblock us after a timeout, so we don't risk blocking too long - // and causing an ANR). - mWaitingUntilKeyguardVisible = true; - mHandler.sendEmptyMessageDelayed(KEYGUARD_DONE_DRAWING, KEYGUARD_DONE_DRAWING_TIMEOUT_MS); - if (DEBUG) Log.d(TAG, "waiting until mWaitingUntilKeyguardVisible is false"); - while (mWaitingUntilKeyguardVisible) { - try { - wait(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } - if (DEBUG) Log.d(TAG, "done waiting for mWaitingUntilKeyguardVisible"); } + if (DEBUG) Log.d(TAG, "done waiting for mWaitingUntilKeyguardVisible"); } } } @@ -1748,13 +1715,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } catch (RemoteException e) { Slog.w(TAG, "Failed to call onKeyguardExitResult(false)", e); } - } else if (mExitSecureCallback != null) { - // already in progress with someone else - try { - callback.onKeyguardExitResult(false); - } catch (RemoteException e) { - Slog.w(TAG, "Failed to call onKeyguardExitResult(false)", e); - } } else if (!isSecure()) { // Keyguard is not secure, no need to do anything, and we don't need to reshow @@ -2289,21 +2249,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, return; } setPendingLock(false); // user may have authenticated during the screen off animation - if (mExitSecureCallback != null) { - try { - mExitSecureCallback.onKeyguardExitResult(true /* authenciated */); - } catch (RemoteException e) { - Slog.w(TAG, "Failed to call onKeyguardExitResult()", e); - } - - mExitSecureCallback = null; - - // after successfully exiting securely, no need to reshow - // the keyguard when they've released the lock - mExternallyEnabled = true; - mNeedToReshowWhenReenabled = false; - updateInputRestricted(); - } handleHide(); mUpdateMonitor.clearBiometricRecognizedWhenKeyguardDone(currentUser); @@ -3111,7 +3056,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, pw.print(" mInputRestricted: "); pw.println(mInputRestricted); pw.print(" mOccluded: "); pw.println(mOccluded); pw.print(" mDelayedShowingSequence: "); pw.println(mDelayedShowingSequence); - pw.print(" mExitSecureCallback: "); pw.println(mExitSecureCallback); pw.print(" mDeviceInteractive: "); pw.println(mDeviceInteractive); pw.print(" mGoingToSleep: "); pw.println(mGoingToSleep); pw.print(" mHiding: "); pw.println(mHiding); diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt new file mode 100644 index 000000000000..a069582f6692 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.keyguard.data.quickaffordance + +/** + * Unique identifier keys for all known built-in quick affordances. + * + * Please ensure uniqueness by never associating more than one class with each key. + */ +object BuiltInKeyguardQuickAffordanceKeys { + // Please keep alphabetical order of const names to simplify future maintenance. + const val HOME_CONTROLS = "home" + const val QR_CODE_SCANNER = "qr_code_scanner" + const val QUICK_ACCESS_WALLET = "wallet" + // Please keep alphabetical order of const names to simplify future maintenance. +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt index 83842602cbee..c600e13cc2dd 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt @@ -1,21 +1,21 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2022 The Android Open Source Project * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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 + * 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. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. * */ -package com.android.systemui.keyguard.domain.quickaffordance +package com.android.systemui.keyguard.data.quickaffordance import android.content.Context import android.content.Intent @@ -51,19 +51,21 @@ constructor( private val appContext = context.applicationContext - override val state: Flow<KeyguardQuickAffordanceConfig.State> = + override val key: String = BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS + + override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState> = component.canShowWhileLockedSetting.flatMapLatest { canShowWhileLocked -> if (canShowWhileLocked) { stateInternal(component.getControlsListingController().getOrNull()) } else { - flowOf(KeyguardQuickAffordanceConfig.State.Hidden) + flowOf(KeyguardQuickAffordanceConfig.LockScreenState.Hidden) } } - override fun onQuickAffordanceClicked( + override fun onTriggered( expandable: Expandable?, - ): KeyguardQuickAffordanceConfig.OnClickedResult { - return KeyguardQuickAffordanceConfig.OnClickedResult.StartActivity( + ): KeyguardQuickAffordanceConfig.OnTriggeredResult { + return KeyguardQuickAffordanceConfig.OnTriggeredResult.StartActivity( intent = Intent(appContext, ControlsActivity::class.java) .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK) @@ -77,9 +79,9 @@ constructor( private fun stateInternal( listingController: ControlsListingController?, - ): Flow<KeyguardQuickAffordanceConfig.State> { + ): Flow<KeyguardQuickAffordanceConfig.LockScreenState> { if (listingController == null) { - return flowOf(KeyguardQuickAffordanceConfig.State.Hidden) + return flowOf(KeyguardQuickAffordanceConfig.LockScreenState.Hidden) } return conflatedCallbackFlow { @@ -114,7 +116,7 @@ constructor( hasServiceInfos: Boolean, visibility: ControlsComponent.Visibility, @DrawableRes iconResourceId: Int?, - ): KeyguardQuickAffordanceConfig.State { + ): KeyguardQuickAffordanceConfig.LockScreenState { return if ( isFeatureEnabled && hasFavorites && @@ -122,7 +124,7 @@ constructor( iconResourceId != null && visibility == ControlsComponent.Visibility.AVAILABLE ) { - KeyguardQuickAffordanceConfig.State.Visible( + KeyguardQuickAffordanceConfig.LockScreenState.Visible( icon = Icon.Resource( res = iconResourceId, @@ -133,7 +135,7 @@ constructor( ), ) } else { - KeyguardQuickAffordanceConfig.State.Hidden + KeyguardQuickAffordanceConfig.LockScreenState.Hidden } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt new file mode 100644 index 000000000000..0a8090bb11ac --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.keyguard.data.quickaffordance + +import android.content.Intent +import com.android.systemui.animation.Expandable +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.keyguard.shared.quickaffordance.ActivationState +import kotlinx.coroutines.flow.Flow + +/** Defines interface that can act as data source for a single quick affordance model. */ +interface KeyguardQuickAffordanceConfig { + + /** Unique identifier for this quick affordance. It must be globally unique. */ + val key: String + + /** + * The ever-changing state of the affordance. + * + * Used to populate the lock screen. + */ + val lockScreenState: Flow<LockScreenState> + + /** + * Notifies that the affordance was clicked by the user. + * + * @param expandable An [Expandable] to use when animating dialogs or activities + * @return An [OnTriggeredResult] telling the caller what to do next + */ + fun onTriggered(expandable: Expandable?): OnTriggeredResult + + /** + * Encapsulates the state of a "quick affordance" in the keyguard bottom area (for example, a + * button on the lock-screen). + */ + sealed class LockScreenState { + + /** No affordance should show up. */ + object Hidden : LockScreenState() + + /** An affordance is visible. */ + data class Visible( + /** An icon for the affordance. */ + val icon: Icon, + /** The activation state of the affordance. */ + val activationState: ActivationState = ActivationState.NotSupported, + ) : LockScreenState() + } + + sealed class OnTriggeredResult { + /** + * Returning this as a result from the [onTriggered] method means that the implementation + * has taken care of the action, the system will do nothing. + */ + object Handled : OnTriggeredResult() + + /** + * Returning this as a result from the [onTriggered] method means that the implementation + * has _not_ taken care of the action and the system should start an activity using the + * given [Intent]. + */ + data class StartActivity( + val intent: Intent, + val canShowWhileLocked: Boolean, + ) : OnTriggeredResult() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt new file mode 100644 index 000000000000..d620b2a1654c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.keyguard.data.quickaffordance + +import com.android.systemui.R +import com.android.systemui.animation.Expandable +import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.qrcodescanner.controller.QRCodeScannerController +import javax.inject.Inject +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow + +/** QR code scanner quick affordance data source. */ +@SysUISingleton +class QrCodeScannerKeyguardQuickAffordanceConfig +@Inject +constructor( + private val controller: QRCodeScannerController, +) : KeyguardQuickAffordanceConfig { + + override val key: String = BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER + + override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState> = + conflatedCallbackFlow { + val callback = + object : QRCodeScannerController.Callback { + override fun onQRCodeScannerActivityChanged() { + trySendWithFailureLogging(state(), TAG) + } + override fun onQRCodeScannerPreferenceChanged() { + trySendWithFailureLogging(state(), TAG) + } + } + + controller.addCallback(callback) + controller.registerQRCodeScannerChangeObservers( + QRCodeScannerController.DEFAULT_QR_CODE_SCANNER_CHANGE, + QRCodeScannerController.QR_CODE_SCANNER_PREFERENCE_CHANGE + ) + // Registering does not push an initial update. + trySendWithFailureLogging(state(), "initial state", TAG) + + awaitClose { + controller.unregisterQRCodeScannerChangeObservers( + QRCodeScannerController.DEFAULT_QR_CODE_SCANNER_CHANGE, + QRCodeScannerController.QR_CODE_SCANNER_PREFERENCE_CHANGE + ) + controller.removeCallback(callback) + } + } + + override fun onTriggered( + expandable: Expandable?, + ): KeyguardQuickAffordanceConfig.OnTriggeredResult { + return KeyguardQuickAffordanceConfig.OnTriggeredResult.StartActivity( + intent = controller.intent, + canShowWhileLocked = true, + ) + } + + private fun state(): KeyguardQuickAffordanceConfig.LockScreenState { + return if (controller.isEnabledForLockScreenButton) { + KeyguardQuickAffordanceConfig.LockScreenState.Visible( + icon = + Icon.Resource( + res = R.drawable.ic_qr_code_scanner, + contentDescription = + ContentDescription.Resource( + res = R.string.accessibility_qr_code_scanner_button, + ), + ), + ) + } else { + KeyguardQuickAffordanceConfig.LockScreenState.Hidden + } + } + + companion object { + private const val TAG = "QrCodeScannerKeyguardQuickAffordanceConfig" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt new file mode 100644 index 000000000000..be57a323b5d1 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.keyguard.data.quickaffordance + +import android.graphics.drawable.Drawable +import android.service.quickaccesswallet.GetWalletCardsError +import android.service.quickaccesswallet.GetWalletCardsResponse +import android.service.quickaccesswallet.QuickAccessWalletClient +import android.util.Log +import com.android.systemui.R +import com.android.systemui.animation.Expandable +import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.wallet.controller.QuickAccessWalletController +import javax.inject.Inject +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow + +/** Quick access wallet quick affordance data source. */ +@SysUISingleton +class QuickAccessWalletKeyguardQuickAffordanceConfig +@Inject +constructor( + private val walletController: QuickAccessWalletController, + private val activityStarter: ActivityStarter, +) : KeyguardQuickAffordanceConfig { + + override val key: String = BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET + + override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState> = + conflatedCallbackFlow { + val callback = + object : QuickAccessWalletClient.OnWalletCardsRetrievedCallback { + override fun onWalletCardsRetrieved(response: GetWalletCardsResponse?) { + trySendWithFailureLogging( + state( + isFeatureEnabled = walletController.isWalletEnabled, + hasCard = response?.walletCards?.isNotEmpty() == true, + tileIcon = walletController.walletClient.tileIcon, + ), + TAG, + ) + } + + override fun onWalletCardRetrievalError(error: GetWalletCardsError?) { + Log.e(TAG, "Wallet card retrieval error, message: \"${error?.message}\"") + trySendWithFailureLogging( + KeyguardQuickAffordanceConfig.LockScreenState.Hidden, + TAG, + ) + } + } + + walletController.setupWalletChangeObservers( + callback, + QuickAccessWalletController.WalletChangeEvent.WALLET_PREFERENCE_CHANGE, + QuickAccessWalletController.WalletChangeEvent.DEFAULT_PAYMENT_APP_CHANGE + ) + walletController.updateWalletPreference() + walletController.queryWalletCards(callback) + + awaitClose { + walletController.unregisterWalletChangeObservers( + QuickAccessWalletController.WalletChangeEvent.WALLET_PREFERENCE_CHANGE, + QuickAccessWalletController.WalletChangeEvent.DEFAULT_PAYMENT_APP_CHANGE + ) + } + } + + override fun onTriggered( + expandable: Expandable?, + ): KeyguardQuickAffordanceConfig.OnTriggeredResult { + walletController.startQuickAccessUiIntent( + activityStarter, + expandable?.activityLaunchController(), + /* hasCard= */ true, + ) + return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled + } + + private fun state( + isFeatureEnabled: Boolean, + hasCard: Boolean, + tileIcon: Drawable?, + ): KeyguardQuickAffordanceConfig.LockScreenState { + return if (isFeatureEnabled && hasCard && tileIcon != null) { + KeyguardQuickAffordanceConfig.LockScreenState.Visible( + icon = + Icon.Loaded( + drawable = tileIcon, + contentDescription = + ContentDescription.Resource( + res = R.string.accessibility_wallet_button, + ), + ), + ) + } else { + KeyguardQuickAffordanceConfig.LockScreenState.Hidden + } + } + + companion object { + private const val TAG = "QuickAccessWalletKeyguardQuickAffordanceConfig" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt index e8532ecfdc37..ab25597b077c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt @@ -20,6 +20,7 @@ import android.animation.AnimatorListenerAdapter import android.animation.ValueAnimator import android.animation.ValueAnimator.AnimatorUpdateListener import android.annotation.FloatRange +import android.os.Trace import android.util.Log import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.shared.model.KeyguardState @@ -157,12 +158,36 @@ class KeyguardTransitionRepository @Inject constructor() { value: Float, transitionState: TransitionState ) { + trace(info, transitionState) + if (transitionState == TransitionState.FINISHED) { currentTransitionInfo = null } _transitions.value = TransitionStep(info.from, info.to, value, transitionState) } + private fun trace(info: TransitionInfo, transitionState: TransitionState) { + if ( + transitionState != TransitionState.STARTED && + transitionState != TransitionState.FINISHED + ) { + return + } + val traceName = + "Transition: ${info.from} -> ${info.to} " + + if (info.animator == null) { + "(manual)" + } else { + "" + } + val traceCookie = traceName.hashCode() + if (transitionState == TransitionState.STARTED) { + Trace.beginAsyncSection(traceName, traceCookie) + } else if (transitionState == TransitionState.FINISHED) { + Trace.endAsyncSection(traceName, traceCookie) + } + } + companion object { private const val TAG = "KeyguardTransitionRepository" } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt index f663b0dd23cd..13d97aaf28da 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt @@ -21,15 +21,14 @@ import android.content.Intent import com.android.internal.widget.LockPatternUtils import com.android.systemui.animation.Expandable import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordanceModel -import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordancePosition -import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceConfig import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceRegistry +import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition import com.android.systemui.plugins.ActivityStarter import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.policy.KeyguardStateController import javax.inject.Inject -import kotlin.reflect.KClass import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.onStart @@ -63,25 +62,25 @@ constructor( } /** - * Notifies that a quick affordance has been clicked by the user. + * Notifies that a quick affordance has been "triggered" (clicked) by the user. * * @param configKey The configuration key corresponding to the [KeyguardQuickAffordanceModel] of * the affordance that was clicked * @param expandable An optional [Expandable] for the activity- or dialog-launch animation */ - fun onQuickAffordanceClicked( - configKey: KClass<out KeyguardQuickAffordanceConfig>, + fun onQuickAffordanceTriggered( + configKey: String, expandable: Expandable?, ) { - @Suppress("UNCHECKED_CAST") val config = registry.get(configKey as KClass<Nothing>) - when (val result = config.onQuickAffordanceClicked(expandable)) { - is KeyguardQuickAffordanceConfig.OnClickedResult.StartActivity -> + @Suppress("UNCHECKED_CAST") val config = registry.get(configKey) + when (val result = config.onTriggered(expandable)) { + is KeyguardQuickAffordanceConfig.OnTriggeredResult.StartActivity -> launchQuickAffordance( intent = result.intent, canShowWhileLocked = result.canShowWhileLocked, expandable = expandable, ) - is KeyguardQuickAffordanceConfig.OnClickedResult.Handled -> Unit + is KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled -> Unit } } @@ -95,16 +94,20 @@ constructor( // value and avoid subtle bugs where the downstream isn't receiving any values // because one config implementation is not emitting an initial value. For example, // see b/244296596. - config.state.onStart { emit(KeyguardQuickAffordanceConfig.State.Hidden) } + config.lockScreenState.onStart { + emit(KeyguardQuickAffordanceConfig.LockScreenState.Hidden) + } } ) { states -> - val index = states.indexOfFirst { it is KeyguardQuickAffordanceConfig.State.Visible } + val index = + states.indexOfFirst { it is KeyguardQuickAffordanceConfig.LockScreenState.Visible } if (index != -1) { - val visibleState = states[index] as KeyguardQuickAffordanceConfig.State.Visible + val visibleState = + states[index] as KeyguardQuickAffordanceConfig.LockScreenState.Visible KeyguardQuickAffordanceModel.Visible( - configKey = configs[index]::class, + configKey = configs[index].key, icon = visibleState.icon, - toggle = visibleState.toggle, + activationState = visibleState.activationState, ) } else { KeyguardQuickAffordanceModel.Hidden diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt index 59bb22786917..7409aec57b4c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt @@ -24,6 +24,8 @@ import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN import com.android.systemui.keyguard.shared.model.TransitionStep import javax.inject.Inject import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge /** Encapsulates business-logic related to the keyguard transitions. */ @SysUISingleton @@ -34,4 +36,17 @@ constructor( ) { /** AOD->LOCKSCREEN transition information. */ val aodToLockscreenTransition: Flow<TransitionStep> = repository.transition(AOD, LOCKSCREEN) + + /** LOCKSCREEN->AOD transition information. */ + val lockscreenToAodTransition: Flow<TransitionStep> = repository.transition(LOCKSCREEN, AOD) + + /** + * AOD<->LOCKSCREEN transition information, mapped to dozeAmount range of AOD (1f) <-> + * Lockscreen (0f). + */ + val dozeAmountTransition: Flow<TransitionStep> = + merge( + aodToLockscreenTransition.map { step -> step.copy(value = 1f - step.value) }, + lockscreenToAodTransition, + ) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardQuickAffordanceModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardQuickAffordanceModel.kt index e56b25967910..32560af6af00 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardQuickAffordanceModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardQuickAffordanceModel.kt @@ -18,9 +18,7 @@ package com.android.systemui.keyguard.domain.model import com.android.systemui.common.shared.model.Icon -import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceConfig -import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordanceToggleState -import kotlin.reflect.KClass +import com.android.systemui.keyguard.shared.quickaffordance.ActivationState /** * Models a "quick affordance" in the keyguard bottom area (for example, a button on the @@ -33,10 +31,10 @@ sealed class KeyguardQuickAffordanceModel { /** A affordance is visible. */ data class Visible( /** Identifier for the affordance this is modeling. */ - val configKey: KClass<out KeyguardQuickAffordanceConfig>, + val configKey: String, /** An icon for the affordance. */ val icon: Icon, - /** The toggle state for the affordance. */ - val toggle: KeyguardQuickAffordanceToggleState, + /** The activation state of the affordance. */ + val activationState: ActivationState, ) : KeyguardQuickAffordanceModel() } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceConfig.kt deleted file mode 100644 index 95027d00c46c..000000000000 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceConfig.kt +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package com.android.systemui.keyguard.domain.quickaffordance - -import android.content.Intent -import com.android.systemui.animation.Expandable -import com.android.systemui.common.shared.model.Icon -import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordanceToggleState -import kotlinx.coroutines.flow.Flow - -/** Defines interface that can act as data source for a single quick affordance model. */ -interface KeyguardQuickAffordanceConfig { - - val state: Flow<State> - - fun onQuickAffordanceClicked(expandable: Expandable?): OnClickedResult - - /** - * Encapsulates the state of a "quick affordance" in the keyguard bottom area (for example, a - * button on the lock-screen). - */ - sealed class State { - - /** No affordance should show up. */ - object Hidden : State() - - /** An affordance is visible. */ - data class Visible( - /** An icon for the affordance. */ - val icon: Icon, - /** The toggle state for the affordance. */ - val toggle: KeyguardQuickAffordanceToggleState = - KeyguardQuickAffordanceToggleState.NotSupported, - ) : State() - } - - sealed class OnClickedResult { - /** - * Returning this as a result from the [onQuickAffordanceClicked] method means that the - * implementation has taken care of the click, the system will do nothing. - */ - object Handled : OnClickedResult() - - /** - * Returning this as a result from the [onQuickAffordanceClicked] method means that the - * implementation has _not_ taken care of the click and the system should start an activity - * using the given [Intent]. - */ - data class StartActivity( - val intent: Intent, - val canShowWhileLocked: Boolean, - ) : OnClickedResult() - } -} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceModule.kt index 94024d4a0ace..b48acb65849e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceModule.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceModule.kt @@ -17,6 +17,7 @@ package com.android.systemui.keyguard.domain.quickaffordance +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig import dagger.Binds import dagger.Module diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceRegistry.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceRegistry.kt index ad40ee7a0183..8526ada69569 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceRegistry.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceRegistry.kt @@ -17,14 +17,17 @@ package com.android.systemui.keyguard.domain.quickaffordance -import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordancePosition +import com.android.systemui.keyguard.data.quickaffordance.HomeControlsKeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.data.quickaffordance.QrCodeScannerKeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.data.quickaffordance.QuickAccessWalletKeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition import javax.inject.Inject -import kotlin.reflect.KClass /** Central registry of all known quick affordance configs. */ interface KeyguardQuickAffordanceRegistry<T : KeyguardQuickAffordanceConfig> { fun getAll(position: KeyguardQuickAffordancePosition): List<T> - fun get(configClass: KClass<out T>): T + fun get(key: String): T } class KeyguardQuickAffordanceRegistryImpl @@ -46,8 +49,8 @@ constructor( qrCodeScanner, ), ) - private val configByClass = - configsByPosition.values.flatten().associateBy { config -> config::class } + private val configByKey = + configsByPosition.values.flatten().associateBy { config -> config.key } override fun getAll( position: KeyguardQuickAffordancePosition, @@ -56,8 +59,8 @@ constructor( } override fun get( - configClass: KClass<out KeyguardQuickAffordanceConfig> + key: String, ): KeyguardQuickAffordanceConfig { - return configByClass.getValue(configClass) + return configByKey.getValue(key) } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt deleted file mode 100644 index 502a6070a422..000000000000 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package com.android.systemui.keyguard.domain.quickaffordance - -import com.android.systemui.R -import com.android.systemui.animation.Expandable -import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow -import com.android.systemui.common.shared.model.ContentDescription -import com.android.systemui.common.shared.model.Icon -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.qrcodescanner.controller.QRCodeScannerController -import javax.inject.Inject -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.Flow - -/** QR code scanner quick affordance data source. */ -@SysUISingleton -class QrCodeScannerKeyguardQuickAffordanceConfig -@Inject -constructor( - private val controller: QRCodeScannerController, -) : KeyguardQuickAffordanceConfig { - - override val state: Flow<KeyguardQuickAffordanceConfig.State> = conflatedCallbackFlow { - val callback = - object : QRCodeScannerController.Callback { - override fun onQRCodeScannerActivityChanged() { - trySendWithFailureLogging(state(), TAG) - } - override fun onQRCodeScannerPreferenceChanged() { - trySendWithFailureLogging(state(), TAG) - } - } - - controller.addCallback(callback) - controller.registerQRCodeScannerChangeObservers( - QRCodeScannerController.DEFAULT_QR_CODE_SCANNER_CHANGE, - QRCodeScannerController.QR_CODE_SCANNER_PREFERENCE_CHANGE - ) - // Registering does not push an initial update. - trySendWithFailureLogging(state(), "initial state", TAG) - - awaitClose { - controller.unregisterQRCodeScannerChangeObservers( - QRCodeScannerController.DEFAULT_QR_CODE_SCANNER_CHANGE, - QRCodeScannerController.QR_CODE_SCANNER_PREFERENCE_CHANGE - ) - controller.removeCallback(callback) - } - } - - override fun onQuickAffordanceClicked( - expandable: Expandable?, - ): KeyguardQuickAffordanceConfig.OnClickedResult { - return KeyguardQuickAffordanceConfig.OnClickedResult.StartActivity( - intent = controller.intent, - canShowWhileLocked = true, - ) - } - - private fun state(): KeyguardQuickAffordanceConfig.State { - return if (controller.isEnabledForLockScreenButton) { - KeyguardQuickAffordanceConfig.State.Visible( - icon = - Icon.Resource( - res = R.drawable.ic_qr_code_scanner, - contentDescription = - ContentDescription.Resource( - res = R.string.accessibility_qr_code_scanner_button, - ), - ), - ) - } else { - KeyguardQuickAffordanceConfig.State.Hidden - } - } - - companion object { - private const val TAG = "QrCodeScannerKeyguardQuickAffordanceConfig" - } -} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt deleted file mode 100644 index a24a0d62465f..000000000000 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package com.android.systemui.keyguard.domain.quickaffordance - -import android.graphics.drawable.Drawable -import android.service.quickaccesswallet.GetWalletCardsError -import android.service.quickaccesswallet.GetWalletCardsResponse -import android.service.quickaccesswallet.QuickAccessWalletClient -import android.util.Log -import com.android.systemui.R -import com.android.systemui.animation.Expandable -import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow -import com.android.systemui.common.shared.model.ContentDescription -import com.android.systemui.common.shared.model.Icon -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.plugins.ActivityStarter -import com.android.systemui.wallet.controller.QuickAccessWalletController -import javax.inject.Inject -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.Flow - -/** Quick access wallet quick affordance data source. */ -@SysUISingleton -class QuickAccessWalletKeyguardQuickAffordanceConfig -@Inject -constructor( - private val walletController: QuickAccessWalletController, - private val activityStarter: ActivityStarter, -) : KeyguardQuickAffordanceConfig { - - override val state: Flow<KeyguardQuickAffordanceConfig.State> = conflatedCallbackFlow { - val callback = - object : QuickAccessWalletClient.OnWalletCardsRetrievedCallback { - override fun onWalletCardsRetrieved(response: GetWalletCardsResponse?) { - trySendWithFailureLogging( - state( - isFeatureEnabled = walletController.isWalletEnabled, - hasCard = response?.walletCards?.isNotEmpty() == true, - tileIcon = walletController.walletClient.tileIcon, - ), - TAG, - ) - } - - override fun onWalletCardRetrievalError(error: GetWalletCardsError?) { - Log.e(TAG, "Wallet card retrieval error, message: \"${error?.message}\"") - trySendWithFailureLogging( - KeyguardQuickAffordanceConfig.State.Hidden, - TAG, - ) - } - } - - walletController.setupWalletChangeObservers( - callback, - QuickAccessWalletController.WalletChangeEvent.WALLET_PREFERENCE_CHANGE, - QuickAccessWalletController.WalletChangeEvent.DEFAULT_PAYMENT_APP_CHANGE - ) - walletController.updateWalletPreference() - walletController.queryWalletCards(callback) - - awaitClose { - walletController.unregisterWalletChangeObservers( - QuickAccessWalletController.WalletChangeEvent.WALLET_PREFERENCE_CHANGE, - QuickAccessWalletController.WalletChangeEvent.DEFAULT_PAYMENT_APP_CHANGE - ) - } - } - - override fun onQuickAffordanceClicked( - expandable: Expandable?, - ): KeyguardQuickAffordanceConfig.OnClickedResult { - walletController.startQuickAccessUiIntent( - activityStarter, - expandable?.activityLaunchController(), - /* hasCard= */ true, - ) - return KeyguardQuickAffordanceConfig.OnClickedResult.Handled - } - - private fun state( - isFeatureEnabled: Boolean, - hasCard: Boolean, - tileIcon: Drawable?, - ): KeyguardQuickAffordanceConfig.State { - return if (isFeatureEnabled && hasCard && tileIcon != null) { - KeyguardQuickAffordanceConfig.State.Visible( - icon = - Icon.Loaded( - drawable = tileIcon, - contentDescription = - ContentDescription.Resource( - res = R.string.accessibility_wallet_button, - ), - ), - ) - } else { - KeyguardQuickAffordanceConfig.State.Hidden - } - } - - companion object { - private const val TAG = "QuickAccessWalletKeyguardQuickAffordanceConfig" - } -} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/quickaffordance/KeyguardQuickAffordanceToggleState.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/quickaffordance/ActivationState.kt index 55d38a41849d..a68d190a3a0c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/quickaffordance/KeyguardQuickAffordanceToggleState.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/quickaffordance/ActivationState.kt @@ -17,12 +17,12 @@ package com.android.systemui.keyguard.shared.quickaffordance -/** Enumerates all possible toggle states for a quick affordance on the lock-screen. */ -sealed class KeyguardQuickAffordanceToggleState { - /** Toggling is not supported. */ - object NotSupported : KeyguardQuickAffordanceToggleState() +/** Enumerates all possible activation states for a quick affordance on the lock-screen. */ +sealed class ActivationState { + /** Activation is not supported. */ + object NotSupported : ActivationState() /** The quick affordance is on. */ - object On : KeyguardQuickAffordanceToggleState() + object Active : ActivationState() /** The quick affordance is off. */ - object Off : KeyguardQuickAffordanceToggleState() + object Inactive : ActivationState() } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardQuickAffordancePosition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/quickaffordance/KeyguardQuickAffordancePosition.kt index 581dafa33df7..a18b036c5189 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardQuickAffordancePosition.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/quickaffordance/KeyguardQuickAffordancePosition.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.keyguard.domain.model +package com.android.systemui.keyguard.shared.quickaffordance /** Enumerates all possible positions for quick affordances that can appear on the lock-screen. */ enum class KeyguardQuickAffordancePosition { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt index 535ca7210244..b6b230441397 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt @@ -22,8 +22,8 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInterac import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordanceModel -import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordancePosition -import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordanceToggleState +import com.android.systemui.keyguard.shared.quickaffordance.ActivationState +import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine @@ -118,13 +118,13 @@ constructor( animateReveal = animateReveal, icon = icon, onClicked = { parameters -> - quickAffordanceInteractor.onQuickAffordanceClicked( + quickAffordanceInteractor.onQuickAffordanceTriggered( configKey = parameters.configKey, expandable = parameters.expandable, ) }, isClickable = isClickable, - isActivated = toggle is KeyguardQuickAffordanceToggleState.On, + isActivated = activationState is ActivationState.Active, ) is KeyguardQuickAffordanceModel.Hidden -> KeyguardQuickAffordanceViewModel() } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt index bf598ba85932..44f48f97b62e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt @@ -18,12 +18,10 @@ package com.android.systemui.keyguard.ui.viewmodel import com.android.systemui.animation.Expandable import com.android.systemui.common.shared.model.Icon -import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceConfig -import kotlin.reflect.KClass /** Models the UI state of a keyguard quick affordance button. */ data class KeyguardQuickAffordanceViewModel( - val configKey: KClass<out KeyguardQuickAffordanceConfig>? = null, + val configKey: String? = null, val isVisible: Boolean = false, /** Whether to animate the transition of the quick affordance from invisible to visible. */ val animateReveal: Boolean = false, @@ -33,7 +31,7 @@ data class KeyguardQuickAffordanceViewModel( val isActivated: Boolean = false, ) { data class OnClickedParameters( - val configKey: KClass<out KeyguardQuickAffordanceConfig>, + val configKey: String, val expandable: Expandable?, ) } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt index 6b46d8f30cad..cbb670ebf02d 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt @@ -43,7 +43,8 @@ import com.android.systemui.dreams.DreamOverlayStateController import com.android.systemui.keyguard.WakefulnessLifecycle import com.android.systemui.media.dream.MediaDreamComplication import com.android.systemui.plugins.statusbar.StatusBarStateController -import com.android.systemui.shade.NotifPanelEvents +import com.android.systemui.shade.ShadeStateEvents +import com.android.systemui.shade.ShadeStateEvents.ShadeStateEventsListener import com.android.systemui.statusbar.CrossFadeHelper import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.SysuiStatusBarStateController @@ -96,7 +97,7 @@ constructor( private val dreamOverlayStateController: DreamOverlayStateController, configurationController: ConfigurationController, wakefulnessLifecycle: WakefulnessLifecycle, - panelEventsEvents: NotifPanelEvents, + panelEventsEvents: ShadeStateEvents, private val secureSettings: SecureSettings, @Main private val handler: Handler, ) { @@ -534,8 +535,8 @@ constructor( mediaHosts.forEach { it?.updateViewVisibility() } } - panelEventsEvents.registerListener( - object : NotifPanelEvents.Listener { + panelEventsEvents.addShadeStateEventsListener( + object : ShadeStateEventsListener { override fun onExpandImmediateChanged(isExpandImmediateEnabled: Boolean) { skipQqsOnExpansion = isExpandImmediateEnabled updateDesiredLocation() diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt index 0a6043793ef6..769494a58842 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt @@ -27,10 +27,11 @@ import com.android.systemui.common.shared.model.Icon /** Utility methods for media tap-to-transfer. */ class MediaTttUtils { companion object { - // Used in CTS tests UpdateMediaTapToTransferSenderDisplayTest and - // UpdateMediaTapToTransferReceiverDisplayTest - const val WINDOW_TITLE = "Media Transfer Chip View" - const val WAKE_REASON = "MEDIA_TRANSFER_ACTIVATED" + const val WINDOW_TITLE_SENDER = "Media Transfer Chip View (Sender)" + const val WINDOW_TITLE_RECEIVER = "Media Transfer Chip View (Receiver)" + + const val WAKE_REASON_SENDER = "MEDIA_TRANSFER_ACTIVATED_SENDER" + const val WAKE_REASON_RECEIVER = "MEDIA_TRANSFER_ACTIVATED_RECEIVER" /** * Returns the information needed to display the icon in [Icon] form. diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt index dc794e66b918..7dd9fb4b9cd5 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt @@ -40,7 +40,6 @@ import com.android.systemui.media.taptotransfer.common.MediaTttLogger import com.android.systemui.media.taptotransfer.common.MediaTttUtils import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.policy.ConfigurationController -import com.android.systemui.temporarydisplay.DEFAULT_TIMEOUT_MILLIS import com.android.systemui.temporarydisplay.TemporaryViewDisplayController import com.android.systemui.temporarydisplay.TemporaryViewInfo import com.android.systemui.util.animation.AnimationUtil.Companion.frames @@ -78,8 +77,6 @@ class MediaTttChipControllerReceiver @Inject constructor( configurationController, powerManager, R.layout.media_ttt_chip_receiver, - MediaTttUtils.WINDOW_TITLE, - MediaTttUtils.WAKE_REASON, ) { @SuppressLint("WrongConstant") // We're allowed to use LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS override val windowLayoutParams = commonWindowLayoutParams.apply { @@ -231,7 +228,7 @@ class MediaTttChipControllerReceiver @Inject constructor( data class ChipReceiverInfo( val routeInfo: MediaRoute2Info, val appIconDrawableOverride: Drawable?, - val appNameOverride: CharSequence? -) : TemporaryViewInfo { - override fun getTimeoutMs() = DEFAULT_TIMEOUT_MILLIS -} + val appNameOverride: CharSequence?, + override val windowTitle: String = MediaTttUtils.WINDOW_TITLE_RECEIVER, + override val wakeReason: String = MediaTttUtils.WAKE_REASON_RECEIVER, +) : TemporaryViewInfo() diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt index 6e596ee1f473..af7317c208ab 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt @@ -43,7 +43,7 @@ enum class ChipStateSender( @StringRes val stringResId: Int?, val transferStatus: TransferStatus, val endItem: SenderEndItem?, - val timeout: Long = DEFAULT_TIMEOUT_MILLIS + val timeout: Int = DEFAULT_TIMEOUT_MILLIS, ) { /** * A state representing that the two devices are close but not close enough to *start* a cast to @@ -223,6 +223,6 @@ sealed class SenderEndItem { // Give the Transfer*Triggered states a longer timeout since those states represent an active // process and we should keep the user informed about it as long as possible (but don't allow it to // continue indefinitely). -private const val TRANSFER_TRIGGERED_TIMEOUT_MILLIS = 30000L +private const val TRANSFER_TRIGGERED_TIMEOUT_MILLIS = 30000 private const val TAG = "ChipStateSender" diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt index 1fa8faeecd82..d1ea2d0c83bd 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt @@ -159,6 +159,9 @@ constructor( } }, vibrationEffect = chipStateSender.transferStatus.vibrationEffect, + windowTitle = MediaTttUtils.WINDOW_TITLE_SENDER, + wakeReason = MediaTttUtils.WAKE_REASON_SENDER, + timeoutMs = chipStateSender.timeout, ) } diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java index c089511a7ce9..50cf63d52909 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java @@ -25,11 +25,16 @@ import static android.app.StatusBarManager.WindowVisibleState; import static android.app.StatusBarManager.windowStateToString; import static android.app.WindowConfiguration.ROTATION_UNDEFINED; import static android.view.Display.DEFAULT_DISPLAY; +import static android.view.InsetsState.ITYPE_BOTTOM_MANDATORY_GESTURES; +import static android.view.InsetsState.ITYPE_BOTTOM_TAPPABLE_ELEMENT; +import static android.view.InsetsState.ITYPE_LEFT_GESTURES; import static android.view.InsetsState.ITYPE_NAVIGATION_BAR; +import static android.view.InsetsState.ITYPE_RIGHT_GESTURES; import static android.view.InsetsState.containsType; import static android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE; import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION; +import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD; import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON; import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL; @@ -77,6 +82,7 @@ import android.telecom.TelecomManager; import android.text.TextUtils; import android.util.Log; import android.view.Display; +import android.view.DisplayCutout; import android.view.Gravity; import android.view.HapticFeedbackConstants; import android.view.InsetsFrameProvider; @@ -240,6 +246,12 @@ public class NavigationBar extends ViewController<NavigationBarView> implements private boolean mTransientShown; private boolean mTransientShownFromGestureOnSystemBar; + /** + * This is to indicate whether the navigation bar button is forced visible. This is true + * when the setup wizard is on display. When that happens, the window frame should be provided + * as insets size directly. + */ + private boolean mIsButtonForceVisible; private int mNavBarMode = NAV_BAR_MODE_3BUTTON; private LightBarController mLightBarController; private final LightBarController mMainLightBarController; @@ -623,6 +635,10 @@ public class NavigationBar extends ViewController<NavigationBarView> implements mView.setTouchHandler(mTouchHandler); setNavBarMode(mNavBarMode); mEdgeBackGestureHandler.setStateChangeCallback(mView::updateStates); + mEdgeBackGestureHandler.setButtonForceVisibleChangeCallback((forceVisible) -> { + mIsButtonForceVisible = forceVisible; + repositionNavigationBar(mCurrentRotation); + }); mNavigationBarTransitions.addListener(this::onBarTransition); mView.updateRotationButton(); @@ -810,7 +826,6 @@ public class NavigationBar extends ViewController<NavigationBarView> implements mLayoutDirection = ld; refreshLayout(ld); } - repositionNavigationBar(rotation); if (canShowSecondaryHandle()) { if (rotation != mCurrentRotation) { @@ -1599,23 +1614,15 @@ public class NavigationBar extends ViewController<NavigationBarView> implements width, height, WindowManager.LayoutParams.TYPE_NAVIGATION_BAR, - WindowManager.LayoutParams.FLAG_TOUCHABLE_WHEN_WAKING - | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH | WindowManager.LayoutParams.FLAG_SLIPPERY, PixelFormat.TRANSLUCENT); lp.gravity = gravity; - if (insetsHeight != -1) { - lp.providedInsets = new InsetsFrameProvider[] { - new InsetsFrameProvider(ITYPE_NAVIGATION_BAR, Insets.of(0, 0, 0, insetsHeight)) - }; - } else { - lp.providedInsets = new InsetsFrameProvider[] { - new InsetsFrameProvider(ITYPE_NAVIGATION_BAR) - }; - } + lp.providedInsets = getInsetsFrameProvider(insetsHeight, userContext); + lp.token = new Binder(); lp.accessibilityTitle = userContext.getString(R.string.nav_bar); lp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_COLOR_SPACE_AGNOSTIC @@ -1628,6 +1635,68 @@ public class NavigationBar extends ViewController<NavigationBarView> implements return lp; } + private InsetsFrameProvider[] getInsetsFrameProvider(int insetsHeight, Context userContext) { + final InsetsFrameProvider navBarProvider; + if (insetsHeight != -1 && !mIsButtonForceVisible) { + navBarProvider = new InsetsFrameProvider( + ITYPE_NAVIGATION_BAR, Insets.of(0, 0, 0, insetsHeight)); + // Use window frame for IME. + navBarProvider.insetsSizeOverrides = new InsetsFrameProvider.InsetsSizeOverride[] { + new InsetsFrameProvider.InsetsSizeOverride(TYPE_INPUT_METHOD, null) + }; + } else { + navBarProvider = new InsetsFrameProvider(ITYPE_NAVIGATION_BAR); + navBarProvider.insetsSizeOverrides = new InsetsFrameProvider.InsetsSizeOverride[]{ + new InsetsFrameProvider.InsetsSizeOverride(TYPE_INPUT_METHOD, null) + }; + } + final boolean navBarTapThrough = userContext.getResources().getBoolean( + com.android.internal.R.bool.config_navBarTapThrough); + final InsetsFrameProvider bottomTappableProvider; + if (navBarTapThrough) { + bottomTappableProvider = new InsetsFrameProvider(ITYPE_BOTTOM_TAPPABLE_ELEMENT, + Insets.of(0, 0, 0, 0)); + } else { + bottomTappableProvider = new InsetsFrameProvider(ITYPE_BOTTOM_TAPPABLE_ELEMENT); + } + + if (!mEdgeBackGestureHandler.isHandlingGestures()) { + // 2/3 button navigation is on. Do not provide any gesture insets here. But need to keep + // the provider to support runtime update. + return new InsetsFrameProvider[] { + navBarProvider, + new InsetsFrameProvider( + ITYPE_BOTTOM_MANDATORY_GESTURES, Insets.NONE), + new InsetsFrameProvider(ITYPE_LEFT_GESTURES, InsetsFrameProvider.SOURCE_DISPLAY, + Insets.NONE, null), + new InsetsFrameProvider(ITYPE_RIGHT_GESTURES, + InsetsFrameProvider.SOURCE_DISPLAY, + Insets.NONE, null), + bottomTappableProvider + }; + } else { + // Gesture navigation + final int gestureHeight = userContext.getResources().getDimensionPixelSize( + com.android.internal.R.dimen.navigation_bar_gesture_height); + final DisplayCutout cutout = userContext.getDisplay().getCutout(); + final int safeInsetsLeft = cutout != null ? cutout.getSafeInsetLeft() : 0; + final int safeInsetsRight = cutout != null ? cutout.getSafeInsetRight() : 0; + return new InsetsFrameProvider[] { + navBarProvider, + new InsetsFrameProvider( + ITYPE_BOTTOM_MANDATORY_GESTURES, Insets.of(0, 0, 0, gestureHeight)), + new InsetsFrameProvider(ITYPE_LEFT_GESTURES, InsetsFrameProvider.SOURCE_DISPLAY, + Insets.of(safeInsetsLeft + + mEdgeBackGestureHandler.getEdgeWidthLeft(), 0, 0, 0), null), + new InsetsFrameProvider(ITYPE_RIGHT_GESTURES, + InsetsFrameProvider.SOURCE_DISPLAY, + Insets.of(0, 0, safeInsetsRight + + mEdgeBackGestureHandler.getEdgeWidthRight(), 0), null), + bottomTappableProvider + }; + } + } + private boolean canShowSecondaryHandle() { return mNavBarMode == NAV_BAR_MODE_GESTURAL && mOrientationHandle != null; } diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java index 709467ffd3b5..10ff48babe16 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java @@ -175,6 +175,7 @@ public class EdgeBackGestureHandler extends CurrentUserTracker private final OverviewProxyService mOverviewProxyService; private final SysUiState mSysUiState; private Runnable mStateChangeCallback; + private Consumer<Boolean> mButtonForceVisibleCallback; private final PluginManager mPluginManager; private final ProtoTracer mProtoTracer; @@ -240,6 +241,7 @@ public class EdgeBackGestureHandler extends CurrentUserTracker private boolean mIsBackGestureAllowed; private boolean mGestureBlockingActivityRunning; private boolean mIsNewBackAffordanceEnabled; + private boolean mIsButtonForceVisible; private InputMonitor mInputMonitor; private InputChannelCompat.InputEventReceiver mInputEventReceiver; @@ -402,12 +404,29 @@ public class EdgeBackGestureHandler extends CurrentUserTracker mStateChangeCallback = callback; } + public void setButtonForceVisibleChangeCallback(Consumer<Boolean> callback) { + mButtonForceVisibleCallback = callback; + } + + public int getEdgeWidthLeft() { + return mEdgeWidthLeft; + } + + public int getEdgeWidthRight() { + return mEdgeWidthRight; + } + public void updateCurrentUserResources() { Resources res = mNavigationModeController.getCurrentUserContext().getResources(); mEdgeWidthLeft = mGestureNavigationSettingsObserver.getLeftSensitivity(res); mEdgeWidthRight = mGestureNavigationSettingsObserver.getRightSensitivity(res); - mIsBackGestureAllowed = - !mGestureNavigationSettingsObserver.areNavigationButtonForcedVisible(); + final boolean previousForceVisible = mIsButtonForceVisible; + mIsButtonForceVisible = + mGestureNavigationSettingsObserver.areNavigationButtonForcedVisible(); + if (previousForceVisible != mIsButtonForceVisible && mButtonForceVisibleCallback != null) { + mButtonForceVisibleCallback.accept(mIsButtonForceVisible); + } + mIsBackGestureAllowed = !mIsButtonForceVisible; final DisplayMetrics dm = res.getDisplayMetrics(); final float defaultGestureHeight = res.getDimension( diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt new file mode 100644 index 000000000000..d247f249e2fd --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.notetask + +import android.app.KeyguardManager +import android.content.Context +import android.os.UserManager +import android.view.KeyEvent +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.util.kotlin.getOrNull +import com.android.wm.shell.floating.FloatingTasks +import java.util.Optional +import javax.inject.Inject + +/** + * Entry point for creating and managing note. + * + * The controller decides how a note is launched based in the device state: locked or unlocked. + * + * Currently, we only support a single task per time. + */ +@SysUISingleton +internal class NoteTaskController +@Inject +constructor( + private val context: Context, + private val intentResolver: NoteTaskIntentResolver, + private val optionalFloatingTasks: Optional<FloatingTasks>, + private val optionalKeyguardManager: Optional<KeyguardManager>, + private val optionalUserManager: Optional<UserManager>, + @NoteTaskEnabledKey private val isEnabled: Boolean, +) { + + fun handleSystemKey(keyCode: Int) { + if (!isEnabled) return + + if (keyCode == KeyEvent.KEYCODE_VIDEO_APP_1) { + showNoteTask() + } + } + + private fun showNoteTask() { + val floatingTasks = optionalFloatingTasks.getOrNull() ?: return + val keyguardManager = optionalKeyguardManager.getOrNull() ?: return + val userManager = optionalUserManager.getOrNull() ?: return + val intent = intentResolver.resolveIntent() ?: return + + // TODO(b/249954038): We should handle direct boot (isUserUnlocked). For now, we do nothing. + if (!userManager.isUserUnlocked) return + + if (keyguardManager.isKeyguardLocked) { + context.startActivity(intent) + } else { + // TODO(b/254606432): Should include Intent.EXTRA_FLOATING_WINDOW_MODE parameter. + floatingTasks.showOrSetStashed(intent) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskEnabledKey.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskEnabledKey.kt new file mode 100644 index 000000000000..e0bf1da2f652 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskEnabledKey.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.notetask + +import javax.inject.Qualifier + +/** Key associated with a [Boolean] flag that enables or disables the note task feature. */ +@Qualifier internal annotation class NoteTaskEnabledKey diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt new file mode 100644 index 000000000000..d84717da3a21 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.notetask + +import com.android.systemui.statusbar.CommandQueue +import com.android.wm.shell.floating.FloatingTasks +import dagger.Lazy +import java.util.Optional +import javax.inject.Inject + +/** Class responsible to "glue" all note task dependencies. */ +internal class NoteTaskInitializer +@Inject +constructor( + private val optionalFloatingTasks: Optional<FloatingTasks>, + private val lazyNoteTaskController: Lazy<NoteTaskController>, + private val commandQueue: CommandQueue, + @NoteTaskEnabledKey private val isEnabled: Boolean, +) { + + private val callbacks = + object : CommandQueue.Callbacks { + override fun handleSystemKey(keyCode: Int) { + lazyNoteTaskController.get().handleSystemKey(keyCode) + } + } + + fun initialize() { + if (isEnabled && optionalFloatingTasks.isPresent) { + commandQueue.addCallback(callbacks) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskIntentResolver.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskIntentResolver.kt new file mode 100644 index 000000000000..98d69910aac3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskIntentResolver.kt @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.notetask + +import android.content.ComponentName +import android.content.Intent +import android.content.pm.ActivityInfo +import android.content.pm.PackageManager +import android.content.pm.PackageManager.ResolveInfoFlags +import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.NOTES_ACTION +import javax.inject.Inject + +/** + * Class responsible to query all apps and find one that can handle the [NOTES_ACTION]. If found, an + * [Intent] ready for be launched will be returned. Otherwise, returns null. + * + * TODO(b/248274123): should be revisited once the notes role is implemented. + */ +internal class NoteTaskIntentResolver +@Inject +constructor( + private val packageManager: PackageManager, +) { + + fun resolveIntent(): Intent? { + val intent = Intent(NOTES_ACTION) + val flags = ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong()) + val infoList = packageManager.queryIntentActivities(intent, flags) + + for (info in infoList) { + val packageName = info.serviceInfo.applicationInfo.packageName ?: continue + val activityName = resolveActivityNameForNotesAction(packageName) ?: continue + + return Intent(NOTES_ACTION) + .setComponent(ComponentName(packageName, activityName)) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + + return null + } + + private fun resolveActivityNameForNotesAction(packageName: String): String? { + val intent = Intent(NOTES_ACTION).setPackage(packageName) + val flags = ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong()) + val resolveInfo = packageManager.resolveActivity(intent, flags) + + val activityInfo = resolveInfo?.activityInfo ?: return null + if (activityInfo.name.isNullOrBlank()) return null + if (!activityInfo.exported) return null + if (!activityInfo.enabled) return null + if (!activityInfo.showWhenLocked) return null + if (!activityInfo.turnScreenOn) return null + + return activityInfo.name + } + + companion object { + // TODO(b/254606432): Use Intent.ACTION_NOTES and Intent.ACTION_NOTES_LOCKED instead. + const val NOTES_ACTION = "android.intent.action.NOTES" + } +} + +private val ActivityInfo.showWhenLocked: Boolean + get() = flags and ActivityInfo.FLAG_SHOW_WHEN_LOCKED != 0 + +private val ActivityInfo.turnScreenOn: Boolean + get() = flags and ActivityInfo.FLAG_TURN_SCREEN_ON != 0 diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt new file mode 100644 index 000000000000..035396a6fc76 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.notetask + +import android.app.KeyguardManager +import android.content.Context +import android.os.UserManager +import androidx.core.content.getSystemService +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags +import dagger.Module +import dagger.Provides +import java.util.* + +/** Compose all dependencies required by Note Task feature. */ +@Module +internal class NoteTaskModule { + + @[Provides NoteTaskEnabledKey] + fun provideIsNoteTaskEnabled(featureFlags: FeatureFlags): Boolean { + return featureFlags.isEnabled(Flags.NOTE_TASKS) + } + + @Provides + fun provideOptionalKeyguardManager(context: Context): Optional<KeyguardManager> { + return Optional.ofNullable(context.getSystemService()) + } + + @Provides + fun provideOptionalUserManager(context: Context): Optional<UserManager> { + return Optional.ofNullable(context.getSystemService()) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index b39175e9f9f8..1c0f05745280 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -135,7 +135,6 @@ import com.android.systemui.biometrics.AuthController; import com.android.systemui.camera.CameraGestureHelper; import com.android.systemui.classifier.Classifier; import com.android.systemui.classifier.FalsingCollector; -import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.DisplayId; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.doze.DozeLog; @@ -231,7 +230,6 @@ import com.android.systemui.statusbar.window.StatusBarWindowStateController; import com.android.systemui.unfold.SysUIUnfoldComponent; import com.android.systemui.util.Compile; import com.android.systemui.util.LargeScreenUtils; -import com.android.systemui.util.ListenerSet; import com.android.systemui.util.Utils; import com.android.systemui.util.time.SystemClock; import com.android.wm.shell.animation.FlingAnimationUtils; @@ -372,7 +370,6 @@ public final class NotificationPanelViewController { private final TapAgainViewController mTapAgainViewController; private final LargeScreenShadeHeaderController mLargeScreenShadeHeaderController; private final RecordingController mRecordingController; - private final PanelEventsEmitter mPanelEventsEmitter; private final boolean mVibrateOnOpening; private final VelocityTracker mVelocityTracker = VelocityTracker.obtain(); private final FlingAnimationUtils mFlingAnimationUtilsClosing; @@ -880,7 +877,6 @@ public final class NotificationPanelViewController { Provider<KeyguardBottomAreaViewController> keyguardBottomAreaViewControllerProvider, KeyguardUnlockAnimationController keyguardUnlockAnimationController, NotificationListContainer notificationListContainer, - PanelEventsEmitter panelEventsEmitter, NotificationStackSizeCalculator notificationStackSizeCalculator, UnlockedScreenOffAnimationController unlockedScreenOffAnimationController, ShadeTransitionController shadeTransitionController, @@ -993,7 +989,6 @@ public final class NotificationPanelViewController { mMediaDataManager = mediaDataManager; mTapAgainViewController = tapAgainViewController; mSysUiState = sysUiState; - mPanelEventsEmitter = panelEventsEmitter; pulseExpansionHandler.setPulseExpandAbortListener(() -> { if (mQs != null) { mQs.animateHeaderSlidingOut(); @@ -1948,7 +1943,7 @@ public final class NotificationPanelViewController { private void setQsExpandImmediate(boolean expandImmediate) { if (expandImmediate != mQsExpandImmediate) { mQsExpandImmediate = expandImmediate; - mPanelEventsEmitter.notifyExpandImmediateChange(expandImmediate); + mShadeExpansionStateManager.notifyExpandImmediateChange(expandImmediate); } } @@ -2674,8 +2669,8 @@ public final class NotificationPanelViewController { // When expanding QS, let's authenticate the user if possible, // this will speed up notification actions. - if (height == 0) { - mCentralSurfaces.requestFaceAuth(false, FaceAuthApiRequestReason.QS_EXPANDED); + if (height == 0 && !mKeyguardStateController.canDismissLockScreen()) { + mUpdateMonitor.requestFaceAuth(FaceAuthApiRequestReason.QS_EXPANDED); } } @@ -3889,7 +3884,7 @@ public final class NotificationPanelViewController { boolean wasRunning = mIsLaunchAnimationRunning; mIsLaunchAnimationRunning = running; if (wasRunning != mIsLaunchAnimationRunning) { - mPanelEventsEmitter.notifyLaunchingActivityChanged(running); + mShadeExpansionStateManager.notifyLaunchingActivityChanged(running); } } @@ -3898,7 +3893,7 @@ public final class NotificationPanelViewController { boolean wasClosing = isClosing(); mClosing = isClosing; if (wasClosing != isClosing) { - mPanelEventsEmitter.notifyPanelCollapsingChanged(isClosing); + mShadeExpansionStateManager.notifyPanelCollapsingChanged(isClosing); } mAmbientState.setIsClosing(isClosing); } @@ -3933,7 +3928,7 @@ public final class NotificationPanelViewController { mShadeLog.v("onMiddleClicked on Keyguard, mDozingOnDown: false"); // Try triggering face auth, this "might" run. Check // KeyguardUpdateMonitor#shouldListenForFace to see when face auth won't run. - boolean didFaceAuthRun = mUpdateMonitor.requestFaceAuth(true, + boolean didFaceAuthRun = mUpdateMonitor.requestFaceAuth( FaceAuthApiRequestReason.NOTIFICATION_PANEL_CLICKED); if (didFaceAuthRun) { @@ -4223,8 +4218,8 @@ public final class NotificationPanelViewController { /** * Sets the dozing state. * - * @param dozing {@code true} when dozing. - * @param animate if transition should be animated. + * @param dozing {@code true} when dozing. + * @param animate if transition should be animated. */ public void setDozing(boolean dozing, boolean animate) { if (dozing == mDozing) return; @@ -4364,35 +4359,35 @@ public final class NotificationPanelViewController { /** * Starts fold to AOD animation. * - * @param startAction invoked when the animation starts. - * @param endAction invoked when the animation finishes, also if it was cancelled. + * @param startAction invoked when the animation starts. + * @param endAction invoked when the animation finishes, also if it was cancelled. * @param cancelAction invoked when the animation is cancelled, before endAction. */ public void startFoldToAodAnimation(Runnable startAction, Runnable endAction, Runnable cancelAction) { mView.animate() - .translationX(0) - .alpha(1f) - .setDuration(ANIMATION_DURATION_FOLD_TO_AOD) - .setInterpolator(EMPHASIZED_DECELERATE) - .setListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animation) { - startAction.run(); - } + .translationX(0) + .alpha(1f) + .setDuration(ANIMATION_DURATION_FOLD_TO_AOD) + .setInterpolator(EMPHASIZED_DECELERATE) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + startAction.run(); + } - @Override - public void onAnimationCancel(Animator animation) { - cancelAction.run(); - } + @Override + public void onAnimationCancel(Animator animation) { + cancelAction.run(); + } - @Override - public void onAnimationEnd(Animator animation) { - endAction.run(); - } - }).setUpdateListener(anim -> { - mKeyguardStatusViewController.animateFoldToAod(anim.getAnimatedFraction()); - }).start(); + @Override + public void onAnimationEnd(Animator animation) { + endAction.run(); + } + }).setUpdateListener(anim -> { + mKeyguardStatusViewController.animateFoldToAod(anim.getAnimatedFraction()); + }).start(); } /** @@ -4752,8 +4747,10 @@ public final class NotificationPanelViewController { /** * Maybe vibrate as panel is opened. * - * @param openingWithTouch Whether the panel is being opened with touch. If the panel is instead - * being opened programmatically (such as by the open panel gesture), we always play haptic. + * @param openingWithTouch Whether the panel is being opened with touch. If the panel is + * instead + * being opened programmatically (such as by the open panel gesture), we + * always play haptic. */ private void maybeVibrateOnOpening(boolean openingWithTouch) { if (mVibrateOnOpening) { @@ -4919,10 +4916,12 @@ public final class NotificationPanelViewController { animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); animator.addListener(new AnimatorListenerAdapter() { private boolean mCancelled; + @Override public void onAnimationCancel(Animator animation) { mCancelled = true; } + @Override public void onAnimationEnd(Animator animation) { mIsSpringBackAnimation = false; @@ -4970,7 +4969,7 @@ public final class NotificationPanelViewController { if (isNaN(h)) { Log.wtf(TAG, "ExpandedHeight set to NaN"); } - mNotificationShadeWindowController.batchApplyWindowLayoutParams(()-> { + mNotificationShadeWindowController.batchApplyWindowLayoutParams(() -> { if (mExpandLatencyTracking && h != 0f) { DejankUtils.postAfterTraversal( () -> mLatencyTracker.onActionEnd(LatencyTracker.ACTION_EXPAND_PANEL)); @@ -5161,7 +5160,7 @@ public final class NotificationPanelViewController { /** * Create an animator that can also overshoot * - * @param targetHeight the target height + * @param targetHeight the target height * @param overshootAmount the amount of overshoot desired */ private ValueAnimator createHeightAnimator(float targetHeight, float overshootAmount) { @@ -5917,49 +5916,11 @@ public final class NotificationPanelViewController { } } - @SysUISingleton - static class PanelEventsEmitter implements NotifPanelEvents { - - private final ListenerSet<Listener> mListeners = new ListenerSet<>(); - - @Inject - PanelEventsEmitter() { - } - - @Override - public void registerListener(@androidx.annotation.NonNull @NonNull Listener listener) { - mListeners.addIfAbsent(listener); - } - - @Override - public void unregisterListener(@androidx.annotation.NonNull @NonNull Listener listener) { - mListeners.remove(listener); - } - - private void notifyLaunchingActivityChanged(boolean isLaunchingActivity) { - for (Listener cb : mListeners) { - cb.onLaunchingActivityChanged(isLaunchingActivity); - } - } - - private void notifyPanelCollapsingChanged(boolean isCollapsing) { - for (NotifPanelEvents.Listener cb : mListeners) { - cb.onPanelCollapsingChanged(isCollapsing); - } - } - - private void notifyExpandImmediateChange(boolean expandImmediateEnabled) { - for (NotifPanelEvents.Listener cb : mListeners) { - cb.onExpandImmediateChanged(expandImmediateEnabled); - } - } - } - /** Handles MotionEvents for the Shade. */ public final class TouchHandler implements View.OnTouchListener { private long mLastTouchDownTime = -1L; - /** @see ViewGroup#onInterceptTouchEvent(MotionEvent) */ + /** @see ViewGroup#onInterceptTouchEvent(MotionEvent) */ public boolean onInterceptTouchEvent(MotionEvent event) { if (SPEW_LOGCAT) { Log.v(TAG, @@ -6158,7 +6119,7 @@ public final class NotificationPanelViewController { mShadeLog.logMotionEvent(event, "onTouch: touch ignored due to instant expanding"); return false; } - if (mTouchDisabled && event.getActionMasked() != MotionEvent.ACTION_CANCEL) { + if (mTouchDisabled && event.getActionMasked() != MotionEvent.ACTION_CANCEL) { mShadeLog.logMotionEvent(event, "onTouch: non-cancel action, touch disabled"); return false; } @@ -6366,3 +6327,4 @@ public final class NotificationPanelViewController { } } } + diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotifPanelEventsModule.java b/packages/SystemUI/src/com/android/systemui/shade/ShadeEventsModule.java index 67723843086a..959c339ab3e5 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotifPanelEventsModule.java +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeEventsModule.java @@ -21,10 +21,9 @@ import com.android.systemui.dagger.SysUISingleton; import dagger.Binds; import dagger.Module; -/** Provides a {@link NotifPanelEvents} in {@link SysUISingleton} scope. */ +/** Provides a {@link ShadeStateEvents} in {@link SysUISingleton} scope. */ @Module -public abstract class NotifPanelEventsModule { +public abstract class ShadeEventsModule { @Binds - abstract NotifPanelEvents bindPanelEvents( - NotificationPanelViewController.PanelEventsEmitter impl); + abstract ShadeStateEvents bindShadeEvents(ShadeExpansionStateManager impl); } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt index 7bba74a8b125..667392c9796e 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt @@ -20,6 +20,7 @@ import android.annotation.IntDef import android.util.Log import androidx.annotation.FloatRange import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.shade.ShadeStateEvents.ShadeStateEventsListener import com.android.systemui.util.Compile import java.util.concurrent.CopyOnWriteArrayList import javax.inject.Inject @@ -30,11 +31,12 @@ import javax.inject.Inject * TODO(b/200063118): Make this class the one source of truth for the state of panel expansion. */ @SysUISingleton -class ShadeExpansionStateManager @Inject constructor() { +class ShadeExpansionStateManager @Inject constructor() : ShadeStateEvents { private val expansionListeners = CopyOnWriteArrayList<ShadeExpansionListener>() private val qsExpansionListeners = CopyOnWriteArrayList<ShadeQsExpansionListener>() private val stateListeners = CopyOnWriteArrayList<ShadeStateListener>() + private val shadeStateEventsListeners = CopyOnWriteArrayList<ShadeStateEventsListener>() @PanelState private var state: Int = STATE_CLOSED @FloatRange(from = 0.0, to = 1.0) private var fraction: Float = 0f @@ -79,6 +81,14 @@ class ShadeExpansionStateManager @Inject constructor() { stateListeners.remove(listener) } + override fun addShadeStateEventsListener(listener: ShadeStateEventsListener) { + shadeStateEventsListeners.addIfAbsent(listener) + } + + override fun removeShadeStateEventsListener(listener: ShadeStateEventsListener) { + shadeStateEventsListeners.remove(listener) + } + /** Returns true if the panel is currently closed and false otherwise. */ fun isClosed(): Boolean = state == STATE_CLOSED @@ -162,6 +172,24 @@ class ShadeExpansionStateManager @Inject constructor() { stateListeners.forEach { it.onPanelStateChanged(state) } } + fun notifyLaunchingActivityChanged(isLaunchingActivity: Boolean) { + for (cb in shadeStateEventsListeners) { + cb.onLaunchingActivityChanged(isLaunchingActivity) + } + } + + fun notifyPanelCollapsingChanged(isCollapsing: Boolean) { + for (cb in shadeStateEventsListeners) { + cb.onPanelCollapsingChanged(isCollapsing) + } + } + + fun notifyExpandImmediateChange(expandImmediateEnabled: Boolean) { + for (cb in shadeStateEventsListeners) { + cb.onExpandImmediateChanged(expandImmediateEnabled) + } + } + private fun debugLog(msg: String) { if (!DEBUG) return Log.v(TAG, msg) diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotifPanelEvents.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeStateEvents.kt index 4558061de1a2..56bb1a6020cf 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotifPanelEvents.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeStateEvents.kt @@ -16,27 +16,25 @@ package com.android.systemui.shade -/** Provides certain notification panel events. */ -interface NotifPanelEvents { +/** Provides certain notification panel events. */ +interface ShadeStateEvents { - /** Registers callbacks to be invoked when notification panel events occur. */ - fun registerListener(listener: Listener) + /** Registers callbacks to be invoked when notification panel events occur. */ + fun addShadeStateEventsListener(listener: ShadeStateEventsListener) - /** Unregisters callbacks previously registered via [registerListener] */ - fun unregisterListener(listener: Listener) + /** Unregisters callbacks previously registered via [addShadeStateEventsListener] */ + fun removeShadeStateEventsListener(listener: ShadeStateEventsListener) /** Callbacks for certain notification panel events. */ - interface Listener { + interface ShadeStateEventsListener { /** Invoked when the notification panel starts or stops collapsing. */ - @JvmDefault - fun onPanelCollapsingChanged(isCollapsing: Boolean) {} + @JvmDefault fun onPanelCollapsingChanged(isCollapsing: Boolean) {} /** * Invoked when the notification panel starts or stops launching an [android.app.Activity]. */ - @JvmDefault - fun onLaunchingActivityChanged(isLaunchingActivity: Boolean) {} + @JvmDefault fun onLaunchingActivityChanged(isLaunchingActivity: Boolean) {} /** * Invoked when the "expand immediate" attribute changes. @@ -47,7 +45,6 @@ interface NotifPanelEvents { * Another example is when full QS is showing, and we swipe up from the bottom. Instead of * going to QQS, the panel fully collapses. */ - @JvmDefault - fun onExpandImmediateChanged(isExpandImmediateEnabled: Boolean) {} + @JvmDefault fun onExpandImmediateChanged(isExpandImmediateEnabled: Boolean) {} } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java index d3bc257d8a54..3002a6820990 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java @@ -28,7 +28,7 @@ import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dump.DumpManager; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.plugins.statusbar.StatusBarStateController; -import com.android.systemui.shade.NotifPanelEvents; +import com.android.systemui.shade.ShadeStateEvents; import com.android.systemui.statusbar.notification.collection.GroupEntry; import com.android.systemui.statusbar.notification.collection.ListEntry; import com.android.systemui.statusbar.notification.collection.NotifPipeline; @@ -55,12 +55,12 @@ import javax.inject.Inject; // TODO(b/204468557): Move to @CoordinatorScope @SysUISingleton public class VisualStabilityCoordinator implements Coordinator, Dumpable, - NotifPanelEvents.Listener { + ShadeStateEvents.ShadeStateEventsListener { public static final String TAG = "VisualStability"; public static final boolean DEBUG = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.VERBOSE); private final DelayableExecutor mDelayableExecutor; private final HeadsUpManager mHeadsUpManager; - private final NotifPanelEvents mNotifPanelEvents; + private final ShadeStateEvents mShadeStateEvents; private final StatusBarStateController mStatusBarStateController; private final VisualStabilityProvider mVisualStabilityProvider; private final WakefulnessLifecycle mWakefulnessLifecycle; @@ -92,7 +92,7 @@ public class VisualStabilityCoordinator implements Coordinator, Dumpable, DelayableExecutor delayableExecutor, DumpManager dumpManager, HeadsUpManager headsUpManager, - NotifPanelEvents notifPanelEvents, + ShadeStateEvents shadeStateEvents, StatusBarStateController statusBarStateController, VisualStabilityProvider visualStabilityProvider, WakefulnessLifecycle wakefulnessLifecycle) { @@ -101,7 +101,7 @@ public class VisualStabilityCoordinator implements Coordinator, Dumpable, mWakefulnessLifecycle = wakefulnessLifecycle; mStatusBarStateController = statusBarStateController; mDelayableExecutor = delayableExecutor; - mNotifPanelEvents = notifPanelEvents; + mShadeStateEvents = shadeStateEvents; dumpManager.registerDumpable(this); } @@ -114,7 +114,7 @@ public class VisualStabilityCoordinator implements Coordinator, Dumpable, mStatusBarStateController.addCallback(mStatusBarStateControllerListener); mPulsing = mStatusBarStateController.isPulsing(); - mNotifPanelEvents.registerListener(this); + mShadeStateEvents.addShadeStateEventsListener(this); pipeline.setVisualStabilityManager(mNotifStabilityManager); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java index da4cceda531f..ff6389141575 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java @@ -32,8 +32,8 @@ import com.android.systemui.dagger.qualifiers.UiBackground; import com.android.systemui.people.widget.PeopleSpaceWidgetManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.settings.UserContextProvider; -import com.android.systemui.shade.NotifPanelEventsModule; import com.android.systemui.shade.ShadeController; +import com.android.systemui.shade.ShadeEventsModule; import com.android.systemui.statusbar.NotificationListener; import com.android.systemui.statusbar.notification.AssistantFeedbackController; import com.android.systemui.statusbar.notification.collection.NotifInflaterImpl; @@ -93,7 +93,7 @@ import dagger.Provides; @Module(includes = { CoordinatorsModule.class, KeyguardNotificationVisibilityProviderModule.class, - NotifPanelEventsModule.class, + ShadeEventsModule.class, NotifPipelineChoreographerModule.class, NotificationSectionHeadersModule.class, }) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java index 70cf56d6d12c..2504fc1c7eb9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java @@ -41,7 +41,6 @@ import androidx.lifecycle.LifecycleOwner; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.statusbar.RegisterStatusBarResult; import com.android.keyguard.AuthKeyguardMessageArea; -import com.android.keyguard.FaceAuthApiRequestReason; import com.android.systemui.Dumpable; import com.android.systemui.animation.ActivityLaunchAnimator; import com.android.systemui.animation.RemoteTransitionAdapter; @@ -230,13 +229,6 @@ public interface CentralSurfaces extends Dumpable, ActivityStarter, LifecycleOwn boolean isShadeDisabled(); - /** - * Request face auth to initiated - * @param userInitiatedRequest Whether this was a user initiated request - * @param reason Reason why face auth was triggered. - */ - void requestFaceAuth(boolean userInitiatedRequest, @FaceAuthApiRequestReason String reason); - @Override void startActivity(Intent intent, boolean onlyProvisioned, boolean dismissShade, int flags); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java index 43bc99fd8e5e..d227ed366ecb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -122,7 +122,6 @@ import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.statusbar.IStatusBarService; import com.android.internal.statusbar.RegisterStatusBarResult; import com.android.keyguard.AuthKeyguardMessageArea; -import com.android.keyguard.FaceAuthApiRequestReason; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.KeyguardUpdateMonitorCallback; import com.android.keyguard.ViewMediatorCallback; @@ -1614,18 +1613,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { return (mDisabled2 & StatusBarManager.DISABLE2_NOTIFICATION_SHADE) != 0; } - /** - * Asks {@link KeyguardUpdateMonitor} to run face auth. - */ - @Override - public void requestFaceAuth(boolean userInitiatedRequest, - @FaceAuthApiRequestReason String reason) { - if (!mKeyguardStateController.canDismissLockScreen()) { - mKeyguardUpdateMonitor.requestFaceAuth( - userInitiatedRequest, reason); - } - } - private void updateReportRejectedTouchVisibility() { if (mReportRejectedTouch == null) { return; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt index 5e26cf062b58..4550cb2987ea 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt @@ -73,7 +73,6 @@ class KeyguardLiftController @Inject constructor( isListening = false updateListeningState() keyguardUpdateMonitor.requestFaceAuth( - true, FaceAuthApiRequestReason.PICK_UP_GESTURE_TRIGGERED ) keyguardUpdateMonitor.requestActiveUnlock( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java index 976710351a44..c189acec2930 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java @@ -649,37 +649,6 @@ public class NotificationIconContainer extends ViewGroup { return mNumDots > 0; } - /** - * If the overflow is in the range [1, max_dots - 1) (basically 1 or 2 dots), then - * extra padding will have to be accounted for - * - * This method has no meaning for non-static containers - */ - public boolean hasPartialOverflow() { - return mNumDots > 0 && mNumDots < MAX_DOTS; - } - - /** - * Get padding that can account for extra dots up to the max. The only valid values for - * this method are for 1 or 2 dots. - * @return only extraDotPadding or extraDotPadding * 2 - */ - public int getPartialOverflowExtraPadding() { - if (!hasPartialOverflow()) { - return 0; - } - - int partialOverflowAmount = (MAX_DOTS - mNumDots) * (mStaticDotDiameter + mDotPadding); - - int adjustedWidth = getFinalTranslationX() + partialOverflowAmount; - // In case we actually give too much padding... - if (adjustedWidth > getWidth()) { - partialOverflowAmount = getWidth() - getFinalTranslationX(); - } - - return partialOverflowAmount; - } - // Give some extra room for btw notifications if we can public int getNoOverflowExtraPadding() { if (mNumDots != 0) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemBarAttributesListener.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemBarAttributesListener.kt index a0415f2f3d7c..6cd8c78dd52f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemBarAttributesListener.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemBarAttributesListener.kt @@ -22,8 +22,6 @@ import android.view.WindowInsetsController.Behavior import com.android.internal.statusbar.LetterboxDetails import com.android.internal.view.AppearanceRegion import com.android.systemui.dump.DumpManager -import com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.Flags import com.android.systemui.statusbar.SysuiStatusBarStateController import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent.CentralSurfacesScope @@ -42,7 +40,6 @@ class SystemBarAttributesListener @Inject internal constructor( private val centralSurfaces: CentralSurfaces, - private val featureFlags: FeatureFlags, private val letterboxAppearanceCalculator: LetterboxAppearanceCalculator, private val statusBarStateController: SysuiStatusBarStateController, private val lightBarController: LightBarController, @@ -127,15 +124,11 @@ internal constructor( } private fun shouldUseLetterboxAppearance(letterboxDetails: Array<LetterboxDetails>) = - isLetterboxAppearanceFlagEnabled() && letterboxDetails.isNotEmpty() - - private fun isLetterboxAppearanceFlagEnabled() = - featureFlags.isEnabled(Flags.STATUS_BAR_LETTERBOX_APPEARANCE) + letterboxDetails.isNotEmpty() private fun dump(printWriter: PrintWriter, strings: Array<String>) { printWriter.println("lastSystemBarAttributesParams: $lastSystemBarAttributesParams") printWriter.println("lastLetterboxAppearance: $lastLetterboxAppearance") - printWriter.println("letterbox appearance flag: ${isLetterboxAppearanceFlagEnabled()}") } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/DataConnectionState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/DataConnectionState.kt new file mode 100644 index 000000000000..da87f7306e60 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/DataConnectionState.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.mobile.data.model + +import android.telephony.TelephonyManager.DATA_CONNECTED +import android.telephony.TelephonyManager.DATA_CONNECTING +import android.telephony.TelephonyManager.DATA_DISCONNECTED +import android.telephony.TelephonyManager.DATA_DISCONNECTING +import android.telephony.TelephonyManager.DataState + +/** Internal enum representation of the telephony data connection states */ +enum class DataConnectionState(@DataState val dataState: Int) { + Connected(DATA_CONNECTED), + Connecting(DATA_CONNECTING), + Disconnected(DATA_DISCONNECTED), + Disconnecting(DATA_DISCONNECTING), +} + +fun @receiver:DataState Int.toDataConnectionType(): DataConnectionState = + when (this) { + DATA_CONNECTED -> DataConnectionState.Connected + DATA_CONNECTING -> DataConnectionState.Connecting + DATA_DISCONNECTED -> DataConnectionState.Disconnected + DATA_DISCONNECTING -> DataConnectionState.Disconnecting + else -> throw IllegalArgumentException("unknown data state received") + } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileSubscriptionModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileSubscriptionModel.kt index eaba0e93e750..6341a114112c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileSubscriptionModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileSubscriptionModel.kt @@ -28,6 +28,7 @@ import android.telephony.TelephonyCallback.SignalStrengthsListener import android.telephony.TelephonyDisplayInfo import android.telephony.TelephonyManager import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN +import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState.Disconnected /** * Data class containing all of the relevant information for a particular line of service, known as @@ -49,14 +50,14 @@ data class MobileSubscriptionModel( @IntRange(from = 0, to = 4) val primaryLevel: Int = CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN, - /** Comes directly from [DataConnectionStateListener.onDataConnectionStateChanged] */ - val dataConnectionState: Int? = null, + /** Mapped from [DataConnectionStateListener.onDataConnectionStateChanged] */ + val dataConnectionState: DataConnectionState = Disconnected, /** From [DataActivityListener.onDataActivity]. See [TelephonyManager] for the values */ @DataActivityType val dataActivityDirection: Int? = null, /** From [CarrierNetworkListener.onCarrierNetworkChange] */ - val carrierNetworkChangeActive: Boolean? = null, + val carrierNetworkChangeActive: Boolean = false, /** * From [DisplayInfoListener.onDisplayInfoChanged]. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt index 45284cf0332b..06e8f467ee0b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt @@ -31,7 +31,9 @@ import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.statusbar.pipeline.mobile.data.model.DefaultNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetworkType +import com.android.systemui.statusbar.pipeline.mobile.data.model.toDataConnectionType import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange import java.lang.IllegalStateException import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher @@ -42,7 +44,7 @@ import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn /** @@ -62,13 +64,15 @@ interface MobileConnectionRepository { * listener + model. */ val subscriptionModelFlow: Flow<MobileSubscriptionModel> + /** Observable tracking [TelephonyManager.isDataConnectionAllowed] */ + val dataEnabled: Flow<Boolean> } @Suppress("EXPERIMENTAL_IS_NOT_ENABLED") @OptIn(ExperimentalCoroutinesApi::class) class MobileConnectionRepositoryImpl( private val subId: Int, - telephonyManager: TelephonyManager, + private val telephonyManager: TelephonyManager, bgDispatcher: CoroutineDispatcher, logger: ConnectivityPipelineLogger, scope: CoroutineScope, @@ -127,7 +131,8 @@ class MobileConnectionRepositoryImpl( dataState: Int, networkType: Int ) { - state = state.copy(dataConnectionState = dataState) + state = + state.copy(dataConnectionState = dataState.toDataConnectionType()) trySend(state) } @@ -160,10 +165,21 @@ class MobileConnectionRepositoryImpl( telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback) awaitClose { telephonyManager.unregisterTelephonyCallback(callback) } } - .onEach { logger.logOutputChange("mobileSubscriptionModel", it.toString()) } + .logOutputChange(logger, "MobileSubscriptionModel") .stateIn(scope, SharingStarted.WhileSubscribed(), state) } + /** + * There are a few cases where we will need to poll [TelephonyManager] so we can update some + * internal state where callbacks aren't provided. Any of those events should be merged into + * this flow, which can be used to trigger the polling. + */ + private val telephonyPollingEvent: Flow<Unit> = subscriptionModelFlow.map {} + + override val dataEnabled: Flow<Boolean> = telephonyPollingEvent.map { dataConnectionAllowed() } + + private fun dataConnectionAllowed(): Boolean = telephonyManager.isDataConnectionAllowed + class Factory @Inject constructor( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt index 15f4acc1127c..f99d278c3903 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt @@ -29,6 +29,9 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map interface MobileIconInteractor { + /** Observable for the data enabled state of this connection */ + val isDataEnabled: Flow<Boolean> + /** Observable for RAT type (network type) indicator */ val networkTypeIconGroup: Flow<MobileIconGroup> @@ -54,6 +57,8 @@ class MobileIconInteractorImpl( ) : MobileIconInteractor { private val mobileStatusInfo = connectionRepository.subscriptionModelFlow + override val isDataEnabled: Flow<Boolean> = connectionRepository.dataEnabled + /** Observable for the current RAT indicator icon ([MobileIconGroup]) */ override val networkTypeIconGroup: Flow<MobileIconGroup> = combine( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt index cd411a4a2afe..614d583c3c48 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt @@ -23,12 +23,14 @@ import com.android.settingslib.SignalIcon.MobileIconGroup import com.android.settingslib.mobile.TelephonyIcons import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepository import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy import com.android.systemui.util.CarrierConfigTracker import javax.inject.Inject import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -47,28 +49,38 @@ import kotlinx.coroutines.flow.stateIn * icon */ interface MobileIconsInteractor { + /** List of subscriptions, potentially filtered for CBRS */ val filteredSubscriptions: Flow<List<SubscriptionInfo>> + /** The icon mapping from network type to [MobileIconGroup] for the default subscription */ val defaultMobileIconMapping: Flow<Map<String, MobileIconGroup>> + /** Fallback [MobileIconGroup] in the case where there is no icon in the mapping */ val defaultMobileIconGroup: Flow<MobileIconGroup> + /** True once the user has been set up */ val isUserSetup: Flow<Boolean> + /** + * Vends out a [MobileIconInteractor] tracking the [MobileConnectionRepository] for the given + * subId. Will throw if the ID is invalid + */ fun createMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor } +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +@OptIn(ExperimentalCoroutinesApi::class) @SysUISingleton class MobileIconsInteractorImpl @Inject constructor( - private val mobileSubscriptionRepo: MobileConnectionsRepository, + private val mobileConnectionsRepo: MobileConnectionsRepository, private val carrierConfigTracker: CarrierConfigTracker, private val mobileMappingsProxy: MobileMappingsProxy, userSetupRepo: UserSetupRepository, @Application private val scope: CoroutineScope, ) : MobileIconsInteractor { private val activeMobileDataSubscriptionId = - mobileSubscriptionRepo.activeMobileDataSubscriptionId + mobileConnectionsRepo.activeMobileDataSubscriptionId private val unfilteredSubscriptions: Flow<List<SubscriptionInfo>> = - mobileSubscriptionRepo.subscriptionsFlow + mobileConnectionsRepo.subscriptionsFlow /** * Generally, SystemUI wants to show iconography for each subscription that is listed by @@ -119,13 +131,13 @@ constructor( * subscription Id. This mapping is the same for every subscription. */ override val defaultMobileIconMapping: StateFlow<Map<String, MobileIconGroup>> = - mobileSubscriptionRepo.defaultDataSubRatConfig + mobileConnectionsRepo.defaultDataSubRatConfig .map { mobileMappingsProxy.mapIconSets(it) } .stateIn(scope, SharingStarted.WhileSubscribed(), initialValue = mapOf()) /** If there is no mapping in [defaultMobileIconMapping], then use this default icon group */ override val defaultMobileIconGroup: StateFlow<MobileIconGroup> = - mobileSubscriptionRepo.defaultDataSubRatConfig + mobileConnectionsRepo.defaultDataSubRatConfig .map { mobileMappingsProxy.getDefaultIcons(it) } .stateIn(scope, SharingStarted.WhileSubscribed(), initialValue = TelephonyIcons.G) @@ -137,6 +149,6 @@ constructor( defaultMobileIconMapping, defaultMobileIconGroup, mobileMappingsProxy, - mobileSubscriptionRepo.getRepoForSubId(subId), + mobileConnectionsRepo.getRepoForSubId(subId), ) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt index cc8f6dd08585..81317398f086 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt @@ -28,7 +28,6 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.map /** * View model for the state of a single mobile icon. Each [MobileIconViewModel] will keep watch over @@ -59,12 +58,18 @@ constructor( /** The RAT icon (LTE, 3G, 5G, etc) to be displayed. Null if we shouldn't show anything */ var networkTypeIcon: Flow<Icon?> = - iconInteractor.networkTypeIconGroup.map { - val desc = - if (it.dataContentDescription != 0) - ContentDescription.Resource(it.dataContentDescription) - else null - Icon.Resource(it.dataType, desc) + combine(iconInteractor.networkTypeIconGroup, iconInteractor.isDataEnabled) { + networkTypeIconGroup, + isDataEnabled -> + if (!isDataEnabled) { + null + } else { + val desc = + if (networkTypeIconGroup.dataContentDescription != 0) + ContentDescription.Resource(networkTypeIconGroup.dataContentDescription) + else null + Icon.Resource(networkTypeIconGroup.dataType, desc) + } } var tint: Flow<Int> = flowOf(Color.CYAN) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityConstants.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityConstants.kt index 118b94c7aa83..6efb10f2a6f2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityConstants.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityConstants.kt @@ -34,7 +34,7 @@ class ConnectivityConstants @Inject constructor(dumpManager: DumpManager, telephonyManager: TelephonyManager) : Dumpable { init { - dumpManager.registerDumpable("$SB_LOGGING_TAG:ConnectivityConstants", this) + dumpManager.registerDumpable("${SB_LOGGING_TAG}Constants", this) } /** True if this device has the capability for data connections and false otherwise. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepository.kt index 6b1750d11562..45c6d466f7e0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepository.kt @@ -62,7 +62,7 @@ class ConnectivityRepositoryImpl @Inject constructor( tunerService: TunerService, ) : ConnectivityRepository, Dumpable { init { - dumpManager.registerDumpable("$SB_LOGGING_TAG:ConnectivityRepository", this) + dumpManager.registerDumpable("${SB_LOGGING_TAG}Repository", this) } // The default set of hidden icons to use if we don't get any from [TunerService]. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/WifiConstants.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/WifiConstants.kt index 0eb4b0de9f6b..3c0eb910ad89 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/WifiConstants.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/WifiConstants.kt @@ -35,7 +35,7 @@ class WifiConstants @Inject constructor( dumpManager: DumpManager, ) : Dumpable { init { - dumpManager.registerDumpable("$SB_LOGGING_TAG:WifiConstants", this) + dumpManager.registerDumpable("${SB_LOGGING_TAG}WifiConstants", this) } /** True if we should show the activityIn/activityOut icons and false otherwise. */ diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt index f0a50de02b3a..637fac05f0b6 100644 --- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt +++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt @@ -44,11 +44,6 @@ import com.android.systemui.util.concurrency.DelayableExecutor * * The generic type T is expected to contain all the information necessary for the subclasses to * display the view in a certain state, since they receive <T> in [updateView]. - * - * @property windowTitle the title to use for the window that displays the temporary view. Should be - * normally cased, like "Window Title". - * @property wakeReason a string used for logging if we needed to wake the screen in order to - * display the temporary view. Should be screaming snake cased, like WAKE_REASON. */ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : TemporaryViewLogger>( internal val context: Context, @@ -59,8 +54,6 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora private val configurationController: ConfigurationController, private val powerManager: PowerManager, @LayoutRes private val viewLayoutRes: Int, - private val windowTitle: String, - private val wakeReason: String, ) : CoreStartable { /** * Window layout params that will be used as a starting point for the [windowLayoutParams] of @@ -72,7 +65,6 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora type = WindowManager.LayoutParams.TYPE_SYSTEM_ERROR flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL - title = windowTitle format = PixelFormat.TRANSLUCENT setTrustedOverlay() } @@ -100,29 +92,40 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora fun displayView(newInfo: T) { val currentDisplayInfo = displayInfo - if (currentDisplayInfo != null) { + if (currentDisplayInfo != null && + currentDisplayInfo.info.windowTitle == newInfo.windowTitle) { + // We're already displaying information in the correctly-titled window, so we just need + // to update the view. currentDisplayInfo.info = newInfo updateView(currentDisplayInfo.info, currentDisplayInfo.view) } else { - // The view is new, so set up all our callbacks and inflate the view + if (currentDisplayInfo != null) { + // We're already displaying information but that information is under a different + // window title. So, we need to remove the old window with the old title and add a + // new window with the new title. + removeView(removalReason = "New info has new window title: ${newInfo.windowTitle}") + } + + // At this point, we're guaranteed to no longer be displaying a view. + // So, set up all our callbacks and inflate the view. configurationController.addCallback(displayScaleListener) // Wake the screen if necessary so the user will see the view. (Per b/239426653, we want // the view to show over the dream state, so we should only wake up if the screen is // completely off.) if (!powerManager.isScreenOn) { powerManager.wakeUp( - SystemClock.uptimeMillis(), - PowerManager.WAKE_REASON_APPLICATION, - "com.android.systemui:$wakeReason", + SystemClock.uptimeMillis(), + PowerManager.WAKE_REASON_APPLICATION, + "com.android.systemui:${newInfo.wakeReason}", ) } - logger.logChipAddition() + logger.logViewAddition(newInfo.windowTitle) inflateAndUpdateView(newInfo) } // Cancel and re-set the view timeout each time we get a new state. val timeout = accessibilityManager.getRecommendedTimeoutMillis( - newInfo.getTimeoutMs().toInt(), + newInfo.timeoutMs, // Not all views have controls so FLAG_CONTENT_CONTROLS might be superfluous, but // include it just to be safe. FLAG_CONTENT_ICONS or FLAG_CONTENT_TEXT or FLAG_CONTENT_CONTROLS @@ -147,7 +150,12 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora val newDisplayInfo = DisplayInfo(newView, newInfo) displayInfo = newDisplayInfo updateView(newDisplayInfo.info, newDisplayInfo.view) - windowManager.addView(newView, windowLayoutParams) + + val paramsWithTitle = WindowManager.LayoutParams().also { + it.copyFrom(windowLayoutParams) + it.title = newInfo.windowTitle + } + windowManager.addView(newView, paramsWithTitle) animateViewIn(newView) } @@ -177,7 +185,7 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora val currentView = currentDisplayInfo.view animateViewOut(currentView) { windowManager.removeView(currentView) } - logger.logChipRemoval(removalReason) + logger.logViewRemoval(removalReason) configurationController.removeCallback(displayScaleListener) // Re-set to null immediately (instead as part of the animation end runnable) so // that if a new view event comes in while this view is animating out, we still display the diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewInfo.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewInfo.kt index 4fe753a80faf..cbb500296888 100644 --- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewInfo.kt +++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewInfo.kt @@ -19,12 +19,24 @@ package com.android.systemui.temporarydisplay /** * A superclass view state used with [TemporaryViewDisplayController]. */ -interface TemporaryViewInfo { +abstract class TemporaryViewInfo { /** - * Returns the amount of time the given view state should display on the screen before it times - * out and disappears. + * The title to use for the window that displays the temporary view. Should be normally cased, + * like "Window Title". */ - fun getTimeoutMs(): Long = DEFAULT_TIMEOUT_MILLIS + abstract val windowTitle: String + + /** + * A string used for logging if we needed to wake the screen in order to display the temporary + * view. Should be screaming snake cased, like WAKE_REASON. + */ + abstract val wakeReason: String + + /** + * The amount of time the given view state should display on the screen before it times out and + * disappears. + */ + open val timeoutMs: Int = DEFAULT_TIMEOUT_MILLIS } -const val DEFAULT_TIMEOUT_MILLIS = 10000L +const val DEFAULT_TIMEOUT_MILLIS = 10000 diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt index a7185cb18c40..428a104484a7 100644 --- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt @@ -24,13 +24,13 @@ open class TemporaryViewLogger( internal val buffer: LogBuffer, internal val tag: String, ) { - /** Logs that we added the chip to a new window. */ - fun logChipAddition() { - buffer.log(tag, LogLevel.DEBUG, {}, { "Chip added" }) + /** Logs that we added the view in a window titled [windowTitle]. */ + fun logViewAddition(windowTitle: String) { + buffer.log(tag, LogLevel.DEBUG, { str1 = windowTitle }, { "View added. window=$str1" }) } /** Logs that we removed the chip for the given [reason]. */ - fun logChipRemoval(reason: String) { - buffer.log(tag, LogLevel.DEBUG, { str1 = reason }, { "Chip removed due to $str1" }) + fun logViewRemoval(reason: String) { + buffer.log(tag, LogLevel.DEBUG, { str1 = reason }, { "View removed due to: $str1" }) } } diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt index b8930a45cd33..87b6e8d3af34 100644 --- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt @@ -38,9 +38,6 @@ import com.android.systemui.common.ui.binder.IconViewBinder import com.android.systemui.common.ui.binder.TextViewBinder import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.media.taptotransfer.common.MediaTttLogger -import com.android.systemui.media.taptotransfer.common.MediaTttUtils -import com.android.systemui.media.taptotransfer.sender.MediaTttSenderLogger import com.android.systemui.plugins.FalsingManager import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.policy.ConfigurationController @@ -64,14 +61,11 @@ import javax.inject.Inject * Only one chipbar may be shown at a time. * TODO(b/245610654): Should we just display whichever chipbar was most recently requested, or do we * need to maintain a priority ordering? - * - * TODO(b/245610654): Remove all media-related items from this class so it's just for generic - * chipbars. */ @SysUISingleton open class ChipbarCoordinator @Inject constructor( context: Context, - @MediaTttSenderLogger logger: MediaTttLogger, + logger: ChipbarLogger, windowManager: WindowManager, @Main mainExecutor: DelayableExecutor, accessibilityManager: AccessibilityManager, @@ -81,7 +75,7 @@ open class ChipbarCoordinator @Inject constructor( private val falsingCollector: FalsingCollector, private val viewUtil: ViewUtil, private val vibratorHelper: VibratorHelper, -) : TemporaryViewDisplayController<ChipbarInfo, MediaTttLogger>( +) : TemporaryViewDisplayController<ChipbarInfo, ChipbarLogger>( context, logger, windowManager, @@ -90,8 +84,6 @@ open class ChipbarCoordinator @Inject constructor( configurationController, powerManager, R.layout.chipbar, - MediaTttUtils.WINDOW_TITLE, - MediaTttUtils.WAKE_REASON, ) { private lateinit var parent: ChipbarRootView @@ -106,7 +98,16 @@ open class ChipbarCoordinator @Inject constructor( newInfo: ChipbarInfo, currentView: ViewGroup ) { - // TODO(b/245610654): Adding logging here. + logger.logViewUpdate( + newInfo.windowTitle, + newInfo.text.loadText(context), + when (newInfo.endItem) { + null -> "null" + is ChipbarEndItem.Loading -> "loading" + is ChipbarEndItem.Error -> "error" + is ChipbarEndItem.Button -> "button(${newInfo.endItem.text.loadText(context)})" + } + ) // Detect falsing touches on the chip. parent = currentView.requireViewById(R.id.chipbar_root_view) diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt index 57fde87114d0..6237365d0cee 100644 --- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt +++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt @@ -37,7 +37,10 @@ data class ChipbarInfo( val text: Text, val endItem: ChipbarEndItem?, val vibrationEffect: VibrationEffect? = null, -) : TemporaryViewInfo + override val windowTitle: String, + override val wakeReason: String, + override val timeoutMs: Int, +) : TemporaryViewInfo() /** The possible items to display at the end of the chipbar. */ sealed class ChipbarEndItem { diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarLogger.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarLogger.kt new file mode 100644 index 000000000000..e477cd68673a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarLogger.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.temporarydisplay.chipbar + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel +import com.android.systemui.temporarydisplay.TemporaryViewLogger +import com.android.systemui.temporarydisplay.dagger.ChipbarLog +import javax.inject.Inject + +/** A logger for the chipbar. */ +@SysUISingleton +class ChipbarLogger +@Inject +constructor( + @ChipbarLog buffer: LogBuffer, +) : TemporaryViewLogger(buffer, "ChipbarLog") { + /** + * Logs that the chipbar was updated to display in a window named [windowTitle], with [text] and + * [endItemDesc]. + */ + fun logViewUpdate(windowTitle: String, text: String?, endItemDesc: String) { + buffer.log( + tag, + LogLevel.DEBUG, + { + str1 = windowTitle + str2 = text + str3 = endItemDesc + }, + { "Chipbar updated. window=$str1 text=$str2 endItem=$str3" } + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/dagger/ChipbarLog.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/dagger/ChipbarLog.kt new file mode 100644 index 000000000000..5f101f2f388d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/dagger/ChipbarLog.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.temporarydisplay.dagger + +import javax.inject.Qualifier + +/** Status bar connectivity logs in table format. */ +@Qualifier +@MustBeDocumented +@kotlin.annotation.Retention(AnnotationRetention.RUNTIME) +annotation class ChipbarLog diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/dagger/TemporaryDisplayModule.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/dagger/TemporaryDisplayModule.kt new file mode 100644 index 000000000000..cf0a1835c8e8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/dagger/TemporaryDisplayModule.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.temporarydisplay.dagger + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.log.LogBufferFactory +import com.android.systemui.plugins.log.LogBuffer +import dagger.Module +import dagger.Provides + +@Module +interface TemporaryDisplayModule { + @Module + companion object { + @JvmStatic + @Provides + @SysUISingleton + @ChipbarLog + fun provideChipbarLogBuffer(factory: LogBufferFactory): LogBuffer { + return factory.create("ChipbarLog", 40) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt new file mode 100644 index 000000000000..9653985cb6e6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.android.systemui.util.kotlin + +import android.view.View +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle +import com.android.systemui.lifecycle.repeatWhenAttached +import java.util.function.Consumer +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collect + +/** + * Collect information for the given [flow], calling [consumer] for each emitted event. Defaults to + * [LifeCycle.State.CREATED] to better align with legacy ViewController usage of attaching listeners + * during onViewAttached() and removing during onViewRemoved() + */ +@JvmOverloads +fun <T> collectFlow( + view: View, + flow: Flow<T>, + consumer: Consumer<T>, + state: Lifecycle.State = Lifecycle.State.CREATED, +) { + view.repeatWhenAttached { repeatOnLifecycle(state) { flow.collect { consumer.accept(it) } } } +} diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java b/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java index 42d7d52a71ab..44f6d03207b1 100644 --- a/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java +++ b/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java @@ -47,7 +47,7 @@ import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.util.concurrency.DelayableExecutor; -import com.android.systemui.wallpapers.canvas.WallpaperColorExtractor; +import com.android.systemui.wallpapers.canvas.WallpaperLocalColorExtractor; import com.android.systemui.wallpapers.gl.EglHelper; import com.android.systemui.wallpapers.gl.ImageWallpaperRenderer; @@ -521,7 +521,7 @@ public class ImageWallpaper extends WallpaperService { class CanvasEngine extends WallpaperService.Engine implements DisplayListener { private WallpaperManager mWallpaperManager; - private final WallpaperColorExtractor mWallpaperColorExtractor; + private final WallpaperLocalColorExtractor mWallpaperLocalColorExtractor; private SurfaceHolder mSurfaceHolder; @VisibleForTesting static final int MIN_SURFACE_WIDTH = 128; @@ -543,9 +543,9 @@ public class ImageWallpaper extends WallpaperService { super(); setFixedSizeAllowed(true); setShowForAllUsers(true); - mWallpaperColorExtractor = new WallpaperColorExtractor( + mWallpaperLocalColorExtractor = new WallpaperLocalColorExtractor( mBackgroundExecutor, - new WallpaperColorExtractor.WallpaperColorExtractorCallback() { + new WallpaperLocalColorExtractor.WallpaperLocalColorExtractorCallback() { @Override public void onColorsProcessed(List<RectF> regions, List<WallpaperColors> colors) { @@ -570,7 +570,7 @@ public class ImageWallpaper extends WallpaperService { // if the number of pages is already computed, transmit it to the color extractor if (mPagesComputed) { - mWallpaperColorExtractor.onPageChanged(mPages); + mWallpaperLocalColorExtractor.onPageChanged(mPages); } } @@ -597,7 +597,7 @@ public class ImageWallpaper extends WallpaperService { public void onDestroy() { getDisplayContext().getSystemService(DisplayManager.class) .unregisterDisplayListener(this); - mWallpaperColorExtractor.cleanUp(); + mWallpaperLocalColorExtractor.cleanUp(); unloadBitmap(); } @@ -813,7 +813,7 @@ public class ImageWallpaper extends WallpaperService { @VisibleForTesting void recomputeColorExtractorMiniBitmap() { - mWallpaperColorExtractor.onBitmapChanged(mBitmap); + mWallpaperLocalColorExtractor.onBitmapChanged(mBitmap); } @VisibleForTesting @@ -830,14 +830,14 @@ public class ImageWallpaper extends WallpaperService { public void addLocalColorsAreas(@NonNull List<RectF> regions) { // this call will activate the offset notifications // if no colors were being processed before - mWallpaperColorExtractor.addLocalColorsAreas(regions); + mWallpaperLocalColorExtractor.addLocalColorsAreas(regions); } @Override public void removeLocalColorsAreas(@NonNull List<RectF> regions) { // this call will deactivate the offset notifications // if we are no longer processing colors - mWallpaperColorExtractor.removeLocalColorAreas(regions); + mWallpaperLocalColorExtractor.removeLocalColorAreas(regions); } @Override @@ -853,7 +853,7 @@ public class ImageWallpaper extends WallpaperService { if (pages != mPages || !mPagesComputed) { mPages = pages; mPagesComputed = true; - mWallpaperColorExtractor.onPageChanged(mPages); + mWallpaperLocalColorExtractor.onPageChanged(mPages); } } @@ -881,7 +881,7 @@ public class ImageWallpaper extends WallpaperService { .getSystemService(WindowManager.class) .getCurrentWindowMetrics() .getBounds(); - mWallpaperColorExtractor.setDisplayDimensions(window.width(), window.height()); + mWallpaperLocalColorExtractor.setDisplayDimensions(window.width(), window.height()); } @@ -902,7 +902,7 @@ public class ImageWallpaper extends WallpaperService { : mBitmap.isRecycled() ? "recycled" : mBitmap.getWidth() + "x" + mBitmap.getHeight()); - mWallpaperColorExtractor.dump(prefix, fd, out, args); + mWallpaperLocalColorExtractor.dump(prefix, fd, out, args); } } } diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/canvas/WallpaperColorExtractor.java b/packages/SystemUI/src/com/android/systemui/wallpapers/canvas/WallpaperLocalColorExtractor.java index e2e4555bb965..6cac5c952b7c 100644 --- a/packages/SystemUI/src/com/android/systemui/wallpapers/canvas/WallpaperColorExtractor.java +++ b/packages/SystemUI/src/com/android/systemui/wallpapers/canvas/WallpaperLocalColorExtractor.java @@ -45,14 +45,14 @@ import java.util.concurrent.Executor; * It uses a background executor, and uses callbacks to inform that the work is done. * It uses a downscaled version of the wallpaper to extract the colors. */ -public class WallpaperColorExtractor { +public class WallpaperLocalColorExtractor { private Bitmap mMiniBitmap; @VisibleForTesting static final int SMALL_SIDE = 128; - private static final String TAG = WallpaperColorExtractor.class.getSimpleName(); + private static final String TAG = WallpaperLocalColorExtractor.class.getSimpleName(); private static final @NonNull RectF LOCAL_COLOR_BOUNDS = new RectF(0, 0, 1, 1); @@ -70,12 +70,12 @@ public class WallpaperColorExtractor { @Background private final Executor mBackgroundExecutor; - private final WallpaperColorExtractorCallback mWallpaperColorExtractorCallback; + private final WallpaperLocalColorExtractorCallback mWallpaperLocalColorExtractorCallback; /** * Interface to handle the callbacks after the different steps of the color extraction */ - public interface WallpaperColorExtractorCallback { + public interface WallpaperLocalColorExtractorCallback { /** * Callback after the colors of new regions have been extracted * @param regions the list of new regions that have been processed @@ -103,13 +103,13 @@ public class WallpaperColorExtractor { /** * Creates a new color extractor. * @param backgroundExecutor the executor on which the color extraction will be performed - * @param wallpaperColorExtractorCallback an interface to handle the callbacks from + * @param wallpaperLocalColorExtractorCallback an interface to handle the callbacks from * the color extractor. */ - public WallpaperColorExtractor(@Background Executor backgroundExecutor, - WallpaperColorExtractorCallback wallpaperColorExtractorCallback) { + public WallpaperLocalColorExtractor(@Background Executor backgroundExecutor, + WallpaperLocalColorExtractorCallback wallpaperLocalColorExtractorCallback) { mBackgroundExecutor = backgroundExecutor; - mWallpaperColorExtractorCallback = wallpaperColorExtractorCallback; + mWallpaperLocalColorExtractorCallback = wallpaperLocalColorExtractorCallback; } /** @@ -157,7 +157,7 @@ public class WallpaperColorExtractor { mBitmapWidth = bitmap.getWidth(); mBitmapHeight = bitmap.getHeight(); mMiniBitmap = createMiniBitmap(bitmap); - mWallpaperColorExtractorCallback.onMiniBitmapUpdated(); + mWallpaperLocalColorExtractorCallback.onMiniBitmapUpdated(); recomputeColors(); } } @@ -206,7 +206,7 @@ public class WallpaperColorExtractor { boolean wasActive = isActive(); mPendingRegions.addAll(regions); if (!wasActive && isActive()) { - mWallpaperColorExtractorCallback.onActivated(); + mWallpaperLocalColorExtractorCallback.onActivated(); } processColorsInternal(); } @@ -228,7 +228,7 @@ public class WallpaperColorExtractor { mPendingRegions.removeAll(regions); regions.forEach(mProcessedRegions::remove); if (wasActive && !isActive()) { - mWallpaperColorExtractorCallback.onDeactivated(); + mWallpaperLocalColorExtractorCallback.onDeactivated(); } } } @@ -252,7 +252,7 @@ public class WallpaperColorExtractor { } private Bitmap createMiniBitmap(@NonNull Bitmap bitmap) { - Trace.beginSection("WallpaperColorExtractor#createMiniBitmap"); + Trace.beginSection("WallpaperLocalColorExtractor#createMiniBitmap"); // if both sides of the image are larger than SMALL_SIDE, downscale the bitmap. int smallestSide = Math.min(bitmap.getWidth(), bitmap.getHeight()); float scale = Math.min(1.0f, (float) SMALL_SIDE / smallestSide); @@ -359,7 +359,7 @@ public class WallpaperColorExtractor { */ if (mDisplayWidth < 0 || mDisplayHeight < 0 || mPages < 0) return; - Trace.beginSection("WallpaperColorExtractor#processColorsInternal"); + Trace.beginSection("WallpaperLocalColorExtractor#processColorsInternal"); List<WallpaperColors> processedColors = new ArrayList<>(); for (int i = 0; i < mPendingRegions.size(); i++) { RectF nextArea = mPendingRegions.get(i); @@ -372,7 +372,7 @@ public class WallpaperColorExtractor { mPendingRegions.clear(); Trace.endSection(); - mWallpaperColorExtractorCallback.onColorsProcessed(processedRegions, processedColors); + mWallpaperLocalColorExtractorCallback.onColorsProcessed(processedRegions, processedColors); } /** diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java index 309f1681b964..02738d5ae48b 100644 --- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java +++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java @@ -49,6 +49,7 @@ import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.keyguard.ScreenLifecycle; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.model.SysUiState; +import com.android.systemui.notetask.NoteTaskInitializer; import com.android.systemui.settings.UserTracker; import com.android.systemui.shared.tracing.ProtoTraceable; import com.android.systemui.statusbar.CommandQueue; @@ -58,7 +59,6 @@ import com.android.systemui.tracing.ProtoTracer; import com.android.systemui.tracing.nano.SystemUiTraceProto; import com.android.wm.shell.desktopmode.DesktopMode; import com.android.wm.shell.desktopmode.DesktopModeTaskRepository; -import com.android.wm.shell.floating.FloatingTasks; import com.android.wm.shell.nano.WmShellTraceProto; import com.android.wm.shell.onehanded.OneHanded; import com.android.wm.shell.onehanded.OneHandedEventCallback; @@ -113,7 +113,6 @@ public final class WMShell implements private final Optional<Pip> mPipOptional; private final Optional<SplitScreen> mSplitScreenOptional; private final Optional<OneHanded> mOneHandedOptional; - private final Optional<FloatingTasks> mFloatingTasksOptional; private final Optional<DesktopMode> mDesktopModeOptional; private final CommandQueue mCommandQueue; @@ -125,6 +124,7 @@ public final class WMShell implements private final WakefulnessLifecycle mWakefulnessLifecycle; private final ProtoTracer mProtoTracer; private final UserTracker mUserTracker; + private final NoteTaskInitializer mNoteTaskInitializer; private final Executor mSysUiMainExecutor; // Listeners and callbacks. Note that we prefer member variable over anonymous class here to @@ -176,7 +176,6 @@ public final class WMShell implements Optional<Pip> pipOptional, Optional<SplitScreen> splitScreenOptional, Optional<OneHanded> oneHandedOptional, - Optional<FloatingTasks> floatingTasksOptional, Optional<DesktopMode> desktopMode, CommandQueue commandQueue, ConfigurationController configurationController, @@ -187,6 +186,7 @@ public final class WMShell implements ProtoTracer protoTracer, WakefulnessLifecycle wakefulnessLifecycle, UserTracker userTracker, + NoteTaskInitializer noteTaskInitializer, @Main Executor sysUiMainExecutor) { mContext = context; mShell = shell; @@ -203,7 +203,7 @@ public final class WMShell implements mWakefulnessLifecycle = wakefulnessLifecycle; mProtoTracer = protoTracer; mUserTracker = userTracker; - mFloatingTasksOptional = floatingTasksOptional; + mNoteTaskInitializer = noteTaskInitializer; mSysUiMainExecutor = sysUiMainExecutor; } @@ -226,6 +226,8 @@ public final class WMShell implements mSplitScreenOptional.ifPresent(this::initSplitScreen); mOneHandedOptional.ifPresent(this::initOneHanded); mDesktopModeOptional.ifPresent(this::initDesktopMode); + + mNoteTaskInitializer.initialize(); } @VisibleForTesting diff --git a/packages/SystemUI/tests/src/com/android/keyguard/FaceWakeUpTriggersConfigTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/FaceWakeUpTriggersConfigTest.kt new file mode 100644 index 000000000000..6c5620d42abb --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/keyguard/FaceWakeUpTriggersConfigTest.kt @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.keyguard + +import android.os.PowerManager +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.dump.DumpManager +import com.android.systemui.util.settings.GlobalSettings +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@RunWith(AndroidTestingRunner::class) +@SmallTest +class FaceWakeUpTriggersConfigTest : SysuiTestCase() { + @Mock lateinit var globalSettings: GlobalSettings + @Mock lateinit var dumpManager: DumpManager + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + } + + @Test + fun testShouldTriggerFaceAuthOnWakeUpFrom_inConfig_returnsTrue() { + val faceWakeUpTriggersConfig = + createFaceWakeUpTriggersConfig( + intArrayOf(PowerManager.WAKE_REASON_POWER_BUTTON, PowerManager.WAKE_REASON_GESTURE) + ) + + assertTrue( + faceWakeUpTriggersConfig.shouldTriggerFaceAuthOnWakeUpFrom( + PowerManager.WAKE_REASON_POWER_BUTTON + ) + ) + assertTrue( + faceWakeUpTriggersConfig.shouldTriggerFaceAuthOnWakeUpFrom( + PowerManager.WAKE_REASON_GESTURE + ) + ) + assertFalse( + faceWakeUpTriggersConfig.shouldTriggerFaceAuthOnWakeUpFrom( + PowerManager.WAKE_REASON_APPLICATION + ) + ) + } + + private fun createFaceWakeUpTriggersConfig(wakeUpTriggers: IntArray): FaceWakeUpTriggersConfig { + overrideResource( + com.android.systemui.R.array.config_face_auth_wake_up_triggers, + wakeUpTriggers + ) + + return FaceWakeUpTriggersConfig(mContext.getResources(), globalSettings, dumpManager) + } +} diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java index b885d546c517..f9bec65ab677 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java @@ -486,7 +486,7 @@ public class KeyguardSecurityContainerControllerTest extends SysuiTestCase { registeredSwipeListener.onSwipeUp(); - verify(mKeyguardUpdateMonitor).requestFaceAuth(true, + verify(mKeyguardUpdateMonitor).requestFaceAuth( FaceAuthApiRequestReason.SWIPE_UP_ON_BOUNCER); } @@ -499,16 +499,15 @@ public class KeyguardSecurityContainerControllerTest extends SysuiTestCase { registeredSwipeListener.onSwipeUp(); verify(mKeyguardUpdateMonitor, never()) - .requestFaceAuth(true, - FaceAuthApiRequestReason.SWIPE_UP_ON_BOUNCER); + .requestFaceAuth(FaceAuthApiRequestReason.SWIPE_UP_ON_BOUNCER); } @Test public void onSwipeUp_whenFaceDetectionIsTriggered_hidesBouncerMessage() { KeyguardSecurityContainer.SwipeListener registeredSwipeListener = getRegisteredSwipeListener(); - when(mKeyguardUpdateMonitor.requestFaceAuth(true, - FaceAuthApiRequestReason.SWIPE_UP_ON_BOUNCER)).thenReturn(true); + when(mKeyguardUpdateMonitor.requestFaceAuth(FaceAuthApiRequestReason.SWIPE_UP_ON_BOUNCER)) + .thenReturn(true); setupGetSecurityView(); registeredSwipeListener.onSwipeUp(); @@ -520,8 +519,8 @@ public class KeyguardSecurityContainerControllerTest extends SysuiTestCase { public void onSwipeUp_whenFaceDetectionIsNotTriggered_retainsBouncerMessage() { KeyguardSecurityContainer.SwipeListener registeredSwipeListener = getRegisteredSwipeListener(); - when(mKeyguardUpdateMonitor.requestFaceAuth(true, - FaceAuthApiRequestReason.SWIPE_UP_ON_BOUNCER)).thenReturn(false); + when(mKeyguardUpdateMonitor.requestFaceAuth(FaceAuthApiRequestReason.SWIPE_UP_ON_BOUNCER)) + .thenReturn(false); setupGetSecurityView(); registeredSwipeListener.onSwipeUp(); diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java index c6233b54c028..680c3b8f546b 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java @@ -110,6 +110,7 @@ import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.telephony.TelephonyListenerManager; +import com.android.systemui.util.settings.GlobalSettings; import org.junit.After; import org.junit.Assert; @@ -210,6 +211,9 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { private UiEventLogger mUiEventLogger; @Mock private PowerManager mPowerManager; + @Mock + private GlobalSettings mGlobalSettings; + private FaceWakeUpTriggersConfig mFaceWakeUpTriggersConfig; private final int mCurrentUserId = 100; private final UserInfo mCurrentUserInfo = new UserInfo(mCurrentUserId, "Test user", 0); @@ -237,8 +241,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { when(mActivityService.getCurrentUser()).thenReturn(mCurrentUserInfo); when(mActivityService.getCurrentUserId()).thenReturn(mCurrentUserId); when(mFaceManager.isHardwareDetected()).thenReturn(true); - when(mFaceManager.hasEnrolledTemplates()).thenReturn(true); - when(mFaceManager.hasEnrolledTemplates(anyInt())).thenReturn(true); + when(mAuthController.isFaceAuthEnrolled(anyInt())).thenReturn(true); when(mFaceManager.getSensorPropertiesInternal()).thenReturn(mFaceSensorProperties); when(mSessionTracker.getSessionId(SESSION_KEYGUARD)).thenReturn(mKeyguardInstanceId); @@ -294,6 +297,12 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { .when(ActivityManager::getCurrentUser); ExtendedMockito.doReturn(mActivityService).when(ActivityManager::getService); + mFaceWakeUpTriggersConfig = new FaceWakeUpTriggersConfig( + mContext.getResources(), + mGlobalSettings, + mDumpManager + ); + mTestableLooper = TestableLooper.get(this); allowTestableLooperAsMainThread(); mKeyguardUpdateMonitor = new TestableKeyguardUpdateMonitor(mContext); @@ -593,7 +602,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { verify(mFaceManager).authenticate(any(), any(), any(), any(), anyInt(), anyBoolean()); verify(mFaceManager).isHardwareDetected(); - verify(mFaceManager).hasEnrolledTemplates(anyInt()); + verify(mFaceManager, never()).hasEnrolledTemplates(anyInt()); } @Test @@ -607,16 +616,22 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { @Test public void testTriesToAuthenticate_whenKeyguard() { - mKeyguardUpdateMonitor.dispatchStartedWakingUp(); - mTestableLooper.processAllMessages(); keyguardIsVisible(); + mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON); + mTestableLooper.processAllMessages(); verify(mFaceManager).authenticate(any(), any(), any(), any(), anyInt(), anyBoolean()); + verify(mUiEventLogger).logWithInstanceIdAndPosition( + eq(FaceAuthUiEvent.FACE_AUTH_UPDATED_STARTED_WAKING_UP), + eq(0), + eq(null), + any(), + eq(PowerManager.WAKE_REASON_POWER_BUTTON)); } @Test public void skipsAuthentication_whenStatusBarShadeLocked() { mStatusBarStateListener.onStateChanged(StatusBarState.SHADE_LOCKED); - mKeyguardUpdateMonitor.dispatchStartedWakingUp(); + mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON); mTestableLooper.processAllMessages(); keyguardIsVisible(); @@ -630,7 +645,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { STRONG_AUTH_REQUIRED_AFTER_BOOT); mKeyguardUpdateMonitor.setKeyguardBypassController(mKeyguardBypassController); - mKeyguardUpdateMonitor.dispatchStartedWakingUp(); + mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON); mTestableLooper.processAllMessages(); keyguardIsVisible(); verify(mFaceManager, never()).authenticate(any(), any(), any(), any(), anyInt(), @@ -661,7 +676,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { bouncerFullyVisibleAndNotGoingToSleep(); mTestableLooper.processAllMessages(); - boolean didFaceAuthRun = mKeyguardUpdateMonitor.requestFaceAuth(true, + boolean didFaceAuthRun = mKeyguardUpdateMonitor.requestFaceAuth( NOTIFICATION_PANEL_CLICKED); assertThat(didFaceAuthRun).isTrue(); @@ -673,7 +688,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { biometricsDisabledForCurrentUser(); mTestableLooper.processAllMessages(); - boolean didFaceAuthRun = mKeyguardUpdateMonitor.requestFaceAuth(true, + boolean didFaceAuthRun = mKeyguardUpdateMonitor.requestFaceAuth( NOTIFICATION_PANEL_CLICKED); assertThat(didFaceAuthRun).isFalse(); @@ -684,7 +699,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { mKeyguardUpdateMonitor.setKeyguardBypassController(mKeyguardBypassController); when(mStrongAuthTracker.getStrongAuthForUser(anyInt())).thenReturn(strongAuth); - mKeyguardUpdateMonitor.dispatchStartedWakingUp(); + mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON); mTestableLooper.processAllMessages(); keyguardIsVisible(); verify(mFaceManager).authenticate(any(), any(), any(), any(), anyInt(), anyBoolean()); @@ -692,8 +707,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { // Stop scanning when bouncer becomes visible setKeyguardBouncerVisibility(true); clearInvocations(mFaceManager); - mKeyguardUpdateMonitor.requestFaceAuth(true, - FaceAuthApiRequestReason.UDFPS_POINTER_DOWN); + mKeyguardUpdateMonitor.requestFaceAuth(FaceAuthApiRequestReason.UDFPS_POINTER_DOWN); verify(mFaceManager, never()).authenticate(any(), any(), any(), any(), anyInt(), anyBoolean()); } @@ -709,7 +723,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { @Test public void testTriesToAuthenticate_whenTrustOnAgentKeyguard_ifBypass() { mKeyguardUpdateMonitor.setKeyguardBypassController(mKeyguardBypassController); - mKeyguardUpdateMonitor.dispatchStartedWakingUp(); + mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON); mTestableLooper.processAllMessages(); when(mKeyguardBypassController.canBypass()).thenReturn(true); mKeyguardUpdateMonitor.onTrustChanged(true /* enabled */, @@ -721,7 +735,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { @Test public void testIgnoresAuth_whenTrustAgentOnKeyguard_withoutBypass() { - mKeyguardUpdateMonitor.dispatchStartedWakingUp(); + mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON); mTestableLooper.processAllMessages(); mKeyguardUpdateMonitor.onTrustChanged(true /* enabled */, KeyguardUpdateMonitor.getCurrentUser(), 0 /* flags */, new ArrayList<>()); @@ -732,7 +746,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { @Test public void testIgnoresAuth_whenLockdown() { - mKeyguardUpdateMonitor.dispatchStartedWakingUp(); + mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON); mTestableLooper.processAllMessages(); when(mStrongAuthTracker.getStrongAuthForUser(anyInt())).thenReturn( KeyguardUpdateMonitor.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN); @@ -744,7 +758,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { @Test public void testTriesToAuthenticate_whenLockout() { - mKeyguardUpdateMonitor.dispatchStartedWakingUp(); + mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON); mTestableLooper.processAllMessages(); when(mStrongAuthTracker.getStrongAuthForUser(anyInt())).thenReturn( KeyguardUpdateMonitor.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT); @@ -768,7 +782,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { @Test public void testFaceAndFingerprintLockout_onlyFace() { - mKeyguardUpdateMonitor.dispatchStartedWakingUp(); + mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON); mTestableLooper.processAllMessages(); keyguardIsVisible(); @@ -779,7 +793,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { @Test public void testFaceAndFingerprintLockout_onlyFingerprint() { - mKeyguardUpdateMonitor.dispatchStartedWakingUp(); + mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON); mTestableLooper.processAllMessages(); keyguardIsVisible(); @@ -791,7 +805,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { @Test public void testFaceAndFingerprintLockout() { - mKeyguardUpdateMonitor.dispatchStartedWakingUp(); + mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON); mTestableLooper.processAllMessages(); keyguardIsVisible(); @@ -890,7 +904,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { when(mFaceManager.getLockoutModeForUser(eq(FACE_SENSOR_ID), eq(newUser))) .thenReturn(faceLockoutMode); - mKeyguardUpdateMonitor.dispatchStartedWakingUp(); + mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON); mTestableLooper.processAllMessages(); keyguardIsVisible(); @@ -1064,7 +1078,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { @Test public void testOccludingAppFingerprintListeningState() { // GIVEN keyguard isn't visible (app occluding) - mKeyguardUpdateMonitor.dispatchStartedWakingUp(); + mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON); mKeyguardUpdateMonitor.setKeyguardShowing(true, true); when(mStrongAuthTracker.hasUserAuthenticatedSinceBoot()).thenReturn(true); @@ -1079,7 +1093,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { @Test public void testOccludingAppRequestsFingerprint() { // GIVEN keyguard isn't visible (app occluding) - mKeyguardUpdateMonitor.dispatchStartedWakingUp(); + mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON); mKeyguardUpdateMonitor.setKeyguardShowing(true, true); // WHEN an occluding app requests fp @@ -1170,7 +1184,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { biometricsNotDisabledThroughDevicePolicyManager(); mStatusBarStateListener.onStateChanged(StatusBarState.SHADE_LOCKED); setKeyguardBouncerVisibility(false /* isVisible */); - mKeyguardUpdateMonitor.dispatchStartedWakingUp(); + mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON); when(mKeyguardBypassController.canBypass()).thenReturn(true); keyguardIsVisible(); @@ -1549,7 +1563,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { @Test public void testFingerprintCanAuth_whenCancellationNotReceivedAndAuthFailed() { - mKeyguardUpdateMonitor.dispatchStartedWakingUp(); + mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON); mTestableLooper.processAllMessages(); keyguardIsVisible(); @@ -1598,6 +1612,36 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { verify(mPowerManager, never()).wakeUp(anyLong(), anyInt(), anyString()); } + @Test + public void testDreamingStopped_faceDoesNotRun() { + mKeyguardUpdateMonitor.dispatchDreamingStopped(); + mTestableLooper.processAllMessages(); + + verify(mFaceManager, never()).authenticate( + any(), any(), any(), any(), anyInt(), anyBoolean()); + } + + @Test + public void testFaceWakeupTrigger_runFaceAuth_onlyOnConfiguredTriggers() { + // keyguard is visible + keyguardIsVisible(); + + // WHEN device wakes up from an application + mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_APPLICATION); + mTestableLooper.processAllMessages(); + + // THEN face auth isn't triggered + verify(mFaceManager, never()).authenticate( + any(), any(), any(), any(), anyInt(), anyBoolean()); + + // WHEN device wakes up from the power button + mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON); + mTestableLooper.processAllMessages(); + + // THEN face auth is triggered + verify(mFaceManager).authenticate(any(), any(), any(), any(), anyInt(), anyBoolean()); + } + private void cleanupKeyguardUpdateMonitor() { if (mKeyguardUpdateMonitor != null) { mKeyguardUpdateMonitor.removeCallback(mTestCallback); @@ -1650,7 +1694,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { } private void triggerSuccessfulFaceAuth() { - mKeyguardUpdateMonitor.requestFaceAuth(true, FaceAuthApiRequestReason.UDFPS_POINTER_DOWN); + mKeyguardUpdateMonitor.requestFaceAuth(FaceAuthApiRequestReason.UDFPS_POINTER_DOWN); verify(mFaceManager).authenticate(any(), any(), mAuthenticationCallbackCaptor.capture(), @@ -1718,7 +1762,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { } private void deviceIsInteractive() { - mKeyguardUpdateMonitor.dispatchStartedWakingUp(); + mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON); } private void bouncerFullyVisible() { @@ -1768,7 +1812,8 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { mKeyguardUpdateMonitorLogger, mUiEventLogger, () -> mSessionTracker, mPowerManager, mTrustManager, mSubscriptionManager, mUserManager, mDreamManager, mDevicePolicyManager, mSensorPrivacyManager, mTelephonyManager, - mPackageManager, mFaceManager, mFingerprintManager, mBiometricManager); + mPackageManager, mFaceManager, mFingerprintManager, mBiometricManager, + mFaceWakeUpTriggersConfig); setStrongAuthTracker(KeyguardUpdateMonitorTest.this.mStrongAuthTracker); } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java new file mode 100644 index 000000000000..ae8f419d4e64 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java @@ -0,0 +1,225 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.keyguard; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; +import static com.android.systemui.flags.Flags.DOZING_MIGRATION_1; + +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.drawable.AnimatedStateListDrawable; +import android.util.Pair; +import android.view.View; +import android.view.WindowManager; +import android.view.accessibility.AccessibilityManager; + +import com.android.systemui.R; +import com.android.systemui.SysuiTestCase; +import com.android.systemui.biometrics.AuthController; +import com.android.systemui.biometrics.AuthRippleController; +import com.android.systemui.doze.util.BurnInHelperKt; +import com.android.systemui.dump.DumpManager; +import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository; +import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository; +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; +import com.android.systemui.plugins.FalsingManager; +import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.statusbar.StatusBarState; +import com.android.systemui.statusbar.VibratorHelper; +import com.android.systemui.statusbar.policy.ConfigurationController; +import com.android.systemui.statusbar.policy.KeyguardStateController; +import com.android.systemui.util.concurrency.FakeExecutor; +import com.android.systemui.util.time.FakeSystemClock; + +import org.junit.After; +import org.junit.Before; +import org.mockito.Answers; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.MockitoSession; +import org.mockito.quality.Strictness; + +public class LockIconViewControllerBaseTest extends SysuiTestCase { + protected static final String UNLOCKED_LABEL = "unlocked"; + protected static final int PADDING = 10; + + protected MockitoSession mStaticMockSession; + + protected @Mock LockIconView mLockIconView; + protected @Mock AnimatedStateListDrawable mIconDrawable; + protected @Mock Context mContext; + protected @Mock Resources mResources; + protected @Mock(answer = Answers.RETURNS_DEEP_STUBS) WindowManager mWindowManager; + protected @Mock StatusBarStateController mStatusBarStateController; + protected @Mock KeyguardUpdateMonitor mKeyguardUpdateMonitor; + protected @Mock KeyguardViewController mKeyguardViewController; + protected @Mock KeyguardStateController mKeyguardStateController; + protected @Mock FalsingManager mFalsingManager; + protected @Mock AuthController mAuthController; + protected @Mock DumpManager mDumpManager; + protected @Mock AccessibilityManager mAccessibilityManager; + protected @Mock ConfigurationController mConfigurationController; + protected @Mock VibratorHelper mVibrator; + protected @Mock AuthRippleController mAuthRippleController; + protected @Mock FeatureFlags mFeatureFlags; + protected @Mock KeyguardTransitionRepository mTransitionRepository; + protected FakeExecutor mDelayableExecutor = new FakeExecutor(new FakeSystemClock()); + + protected LockIconViewController mUnderTest; + + // Capture listeners so that they can be used to send events + @Captor protected ArgumentCaptor<View.OnAttachStateChangeListener> mAttachCaptor = + ArgumentCaptor.forClass(View.OnAttachStateChangeListener.class); + + @Captor protected ArgumentCaptor<KeyguardStateController.Callback> mKeyguardStateCaptor = + ArgumentCaptor.forClass(KeyguardStateController.Callback.class); + protected KeyguardStateController.Callback mKeyguardStateCallback; + + @Captor protected ArgumentCaptor<StatusBarStateController.StateListener> mStatusBarStateCaptor = + ArgumentCaptor.forClass(StatusBarStateController.StateListener.class); + protected StatusBarStateController.StateListener mStatusBarStateListener; + + @Captor protected ArgumentCaptor<AuthController.Callback> mAuthControllerCallbackCaptor; + protected AuthController.Callback mAuthControllerCallback; + + @Captor protected ArgumentCaptor<KeyguardUpdateMonitorCallback> + mKeyguardUpdateMonitorCallbackCaptor = + ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback.class); + protected KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback; + + @Captor protected ArgumentCaptor<Point> mPointCaptor; + + @Before + public void setUp() throws Exception { + mStaticMockSession = mockitoSession() + .mockStatic(BurnInHelperKt.class) + .strictness(Strictness.LENIENT) + .startMocking(); + MockitoAnnotations.initMocks(this); + + setupLockIconViewMocks(); + when(mContext.getResources()).thenReturn(mResources); + when(mContext.getSystemService(WindowManager.class)).thenReturn(mWindowManager); + Rect windowBounds = new Rect(0, 0, 800, 1200); + when(mWindowManager.getCurrentWindowMetrics().getBounds()).thenReturn(windowBounds); + when(mResources.getString(R.string.accessibility_unlock_button)).thenReturn(UNLOCKED_LABEL); + when(mResources.getDrawable(anyInt(), any())).thenReturn(mIconDrawable); + when(mResources.getDimensionPixelSize(R.dimen.lock_icon_padding)).thenReturn(PADDING); + when(mAuthController.getScaleFactor()).thenReturn(1f); + + when(mKeyguardStateController.isShowing()).thenReturn(true); + when(mKeyguardStateController.isKeyguardGoingAway()).thenReturn(false); + when(mStatusBarStateController.isDozing()).thenReturn(false); + when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD); + + mUnderTest = new LockIconViewController( + mLockIconView, + mStatusBarStateController, + mKeyguardUpdateMonitor, + mKeyguardViewController, + mKeyguardStateController, + mFalsingManager, + mAuthController, + mDumpManager, + mAccessibilityManager, + mConfigurationController, + mDelayableExecutor, + mVibrator, + mAuthRippleController, + mResources, + new KeyguardTransitionInteractor(mTransitionRepository), + new KeyguardInteractor(new FakeKeyguardRepository()), + mFeatureFlags + ); + } + + @After + public void tearDown() { + mStaticMockSession.finishMocking(); + } + + protected Pair<Float, Point> setupUdfps() { + when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(true); + final Point udfpsLocation = new Point(50, 75); + final float radius = 33f; + when(mAuthController.getUdfpsLocation()).thenReturn(udfpsLocation); + when(mAuthController.getUdfpsRadius()).thenReturn(radius); + + return new Pair(radius, udfpsLocation); + } + + protected void setupShowLockIcon() { + when(mKeyguardStateController.isShowing()).thenReturn(true); + when(mKeyguardStateController.isKeyguardGoingAway()).thenReturn(false); + when(mStatusBarStateController.isDozing()).thenReturn(false); + when(mStatusBarStateController.getDozeAmount()).thenReturn(0f); + when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD); + when(mKeyguardStateController.canDismissLockScreen()).thenReturn(false); + } + + protected void captureAuthControllerCallback() { + verify(mAuthController).addCallback(mAuthControllerCallbackCaptor.capture()); + mAuthControllerCallback = mAuthControllerCallbackCaptor.getValue(); + } + + protected void captureKeyguardStateCallback() { + verify(mKeyguardStateController).addCallback(mKeyguardStateCaptor.capture()); + mKeyguardStateCallback = mKeyguardStateCaptor.getValue(); + } + + protected void captureStatusBarStateListener() { + verify(mStatusBarStateController).addCallback(mStatusBarStateCaptor.capture()); + mStatusBarStateListener = mStatusBarStateCaptor.getValue(); + } + + protected void captureKeyguardUpdateMonitorCallback() { + verify(mKeyguardUpdateMonitor).registerCallback( + mKeyguardUpdateMonitorCallbackCaptor.capture()); + mKeyguardUpdateMonitorCallback = mKeyguardUpdateMonitorCallbackCaptor.getValue(); + } + + protected void setupLockIconViewMocks() { + when(mLockIconView.getResources()).thenReturn(mResources); + when(mLockIconView.getContext()).thenReturn(mContext); + } + + protected void resetLockIconView() { + reset(mLockIconView); + setupLockIconViewMocks(); + } + + protected void init(boolean useMigrationFlag) { + when(mFeatureFlags.isEnabled(DOZING_MIGRATION_1)).thenReturn(useMigrationFlag); + mUnderTest.init(); + + verify(mLockIconView, atLeast(1)).addOnAttachStateChangeListener(mAttachCaptor.capture()); + mAttachCaptor.getValue().onViewAttachedToWindow(mLockIconView); + } +} diff --git a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerTest.java new file mode 100644 index 000000000000..f4c2284de2d3 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerTest.java @@ -0,0 +1,269 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.keyguard; + +import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT; + +import static com.android.keyguard.LockIconView.ICON_LOCK; +import static com.android.keyguard.LockIconView.ICON_UNLOCK; + +import static org.mockito.Mockito.anyBoolean; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.graphics.Point; +import android.hardware.biometrics.BiometricSourceType; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.util.Pair; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.doze.util.BurnInHelperKt; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper +public class LockIconViewControllerTest extends LockIconViewControllerBaseTest { + + @Test + public void testUpdateFingerprintLocationOnInit() { + // GIVEN fp sensor location is available pre-attached + Pair<Float, Point> udfps = setupUdfps(); // first = radius, second = udfps location + + // WHEN lock icon view controller is initialized and attached + init(/* useMigrationFlag= */false); + + // THEN lock icon view location is updated to the udfps location with UDFPS radius + verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first), + eq(PADDING)); + } + + @Test + public void testUpdatePaddingBasedOnResolutionScale() { + // GIVEN fp sensor location is available pre-attached & scaled resolution factor is 5 + Pair<Float, Point> udfps = setupUdfps(); // first = radius, second = udfps location + when(mAuthController.getScaleFactor()).thenReturn(5f); + + // WHEN lock icon view controller is initialized and attached + init(/* useMigrationFlag= */false); + + // THEN lock icon view location is updated with the scaled radius + verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first), + eq(PADDING * 5)); + } + + @Test + public void testUpdateLockIconLocationOnAuthenticatorsRegistered() { + // GIVEN fp sensor location is not available pre-init + when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(false); + when(mAuthController.getFingerprintSensorLocation()).thenReturn(null); + init(/* useMigrationFlag= */false); + resetLockIconView(); // reset any method call counts for when we verify method calls later + + // GIVEN fp sensor location is available post-attached + captureAuthControllerCallback(); + Pair<Float, Point> udfps = setupUdfps(); + + // WHEN all authenticators are registered + mAuthControllerCallback.onAllAuthenticatorsRegistered(TYPE_FINGERPRINT); + mDelayableExecutor.runAllReady(); + + // THEN lock icon view location is updated with the same coordinates as auth controller vals + verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first), + eq(PADDING)); + } + + @Test + public void testUpdateLockIconLocationOnUdfpsLocationChanged() { + // GIVEN fp sensor location is not available pre-init + when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(false); + when(mAuthController.getFingerprintSensorLocation()).thenReturn(null); + init(/* useMigrationFlag= */false); + resetLockIconView(); // reset any method call counts for when we verify method calls later + + // GIVEN fp sensor location is available post-attached + captureAuthControllerCallback(); + Pair<Float, Point> udfps = setupUdfps(); + + // WHEN udfps location changes + mAuthControllerCallback.onUdfpsLocationChanged(); + mDelayableExecutor.runAllReady(); + + // THEN lock icon view location is updated with the same coordinates as auth controller vals + verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first), + eq(PADDING)); + } + + @Test + public void testLockIconViewBackgroundEnabledWhenUdfpsIsSupported() { + // GIVEN Udpfs sensor location is available + setupUdfps(); + + // WHEN the view is attached + init(/* useMigrationFlag= */false); + + // THEN the lock icon view background should be enabled + verify(mLockIconView).setUseBackground(true); + } + + @Test + public void testLockIconViewBackgroundDisabledWhenUdfpsIsNotSupported() { + // GIVEN Udfps sensor location is not supported + when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(false); + + // WHEN the view is attached + init(/* useMigrationFlag= */false); + + // THEN the lock icon view background should be disabled + verify(mLockIconView).setUseBackground(false); + } + + @Test + public void testUnlockIconShows_biometricUnlockedTrue() { + // GIVEN UDFPS sensor location is available + setupUdfps(); + + // GIVEN lock icon controller is initialized and view is attached + init(/* useMigrationFlag= */false); + captureKeyguardUpdateMonitorCallback(); + + // GIVEN user has unlocked with a biometric auth (ie: face auth) + when(mKeyguardUpdateMonitor.getUserUnlockedWithBiometric(anyInt())).thenReturn(true); + reset(mLockIconView); + + // WHEN face auth's biometric running state changes + mKeyguardUpdateMonitorCallback.onBiometricRunningStateChanged(false, + BiometricSourceType.FACE); + + // THEN the unlock icon is shown + verify(mLockIconView).setContentDescription(UNLOCKED_LABEL); + } + + @Test + public void testLockIconStartState() { + // GIVEN lock icon state + setupShowLockIcon(); + + // WHEN lock icon controller is initialized + init(/* useMigrationFlag= */false); + + // THEN the lock icon should show + verify(mLockIconView).updateIcon(ICON_LOCK, false); + } + + @Test + public void testLockIcon_updateToUnlock() { + // GIVEN starting state for the lock icon + setupShowLockIcon(); + + // GIVEN lock icon controller is initialized and view is attached + init(/* useMigrationFlag= */false); + captureKeyguardStateCallback(); + reset(mLockIconView); + + // WHEN the unlocked state changes to canDismissLockScreen=true + when(mKeyguardStateController.canDismissLockScreen()).thenReturn(true); + mKeyguardStateCallback.onUnlockedChanged(); + + // THEN the unlock should show + verify(mLockIconView).updateIcon(ICON_UNLOCK, false); + } + + @Test + public void testLockIcon_clearsIconOnAod_whenUdfpsNotEnrolled() { + // GIVEN udfps not enrolled + setupUdfps(); + when(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(false); + + // GIVEN starting state for the lock icon + setupShowLockIcon(); + + // GIVEN lock icon controller is initialized and view is attached + init(/* useMigrationFlag= */false); + captureStatusBarStateListener(); + reset(mLockIconView); + + // WHEN the dozing state changes + mStatusBarStateListener.onDozingChanged(true /* isDozing */); + + // THEN the icon is cleared + verify(mLockIconView).clearIcon(); + } + + @Test + public void testLockIcon_updateToAodLock_whenUdfpsEnrolled() { + // GIVEN udfps enrolled + setupUdfps(); + when(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(true); + + // GIVEN starting state for the lock icon + setupShowLockIcon(); + + // GIVEN lock icon controller is initialized and view is attached + init(/* useMigrationFlag= */false); + captureStatusBarStateListener(); + reset(mLockIconView); + + // WHEN the dozing state changes + mStatusBarStateListener.onDozingChanged(true /* isDozing */); + + // THEN the AOD lock icon should show + verify(mLockIconView).updateIcon(ICON_LOCK, true); + } + + @Test + public void testBurnInOffsetsUpdated_onDozeAmountChanged() { + // GIVEN udfps enrolled + setupUdfps(); + when(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(true); + + // GIVEN burn-in offset = 5 + int burnInOffset = 5; + when(BurnInHelperKt.getBurnInOffset(anyInt(), anyBoolean())).thenReturn(burnInOffset); + + // GIVEN starting state for the lock icon (keyguard) + setupShowLockIcon(); + init(/* useMigrationFlag= */false); + captureStatusBarStateListener(); + reset(mLockIconView); + + // WHEN dozing updates + mStatusBarStateListener.onDozingChanged(true /* isDozing */); + mStatusBarStateListener.onDozeAmountChanged(1f, 1f); + + // THEN the view's translation is updated to use the AoD burn-in offsets + verify(mLockIconView).setTranslationY(burnInOffset); + verify(mLockIconView).setTranslationX(burnInOffset); + reset(mLockIconView); + + // WHEN the device is no longer dozing + mStatusBarStateListener.onDozingChanged(false /* isDozing */); + mStatusBarStateListener.onDozeAmountChanged(0f, 0f); + + // THEN the view is updated to NO translation (no burn-in offsets anymore) + verify(mLockIconView).setTranslationY(0); + verify(mLockIconView).setTranslationX(0); + + } +} diff --git a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerWithCoroutinesTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerWithCoroutinesTest.kt new file mode 100644 index 000000000000..d2c54b4cc0e7 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerWithCoroutinesTest.kt @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.keyguard + +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.keyguard.LockIconView.ICON_LOCK +import com.android.systemui.doze.util.getBurnInOffset +import com.android.systemui.keyguard.shared.model.KeyguardState.AOD +import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN +import com.android.systemui.keyguard.shared.model.TransitionState.FINISHED +import com.android.systemui.keyguard.shared.model.TransitionStep +import com.android.systemui.util.mockito.whenever +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.anyBoolean +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.reset +import org.mockito.Mockito.verify + +@RunWith(AndroidTestingRunner::class) +@SmallTest +class LockIconViewControllerWithCoroutinesTest : LockIconViewControllerBaseTest() { + + /** After migration, replaces LockIconViewControllerTest version */ + @Test + fun testLockIcon_clearsIconOnAod_whenUdfpsNotEnrolled() = + runBlocking(IMMEDIATE) { + // GIVEN udfps not enrolled + setupUdfps() + whenever(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(false) + + // GIVEN starting state for the lock icon + setupShowLockIcon() + + // GIVEN lock icon controller is initialized and view is attached + init(/* useMigrationFlag= */ true) + reset(mLockIconView) + + // WHEN the dozing state changes + mUnderTest.mIsDozingCallback.accept(true) + + // THEN the icon is cleared + verify(mLockIconView).clearIcon() + } + + /** After migration, replaces LockIconViewControllerTest version */ + @Test + fun testLockIcon_updateToAodLock_whenUdfpsEnrolled() = + runBlocking(IMMEDIATE) { + // GIVEN udfps enrolled + setupUdfps() + whenever(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(true) + + // GIVEN starting state for the lock icon + setupShowLockIcon() + + // GIVEN lock icon controller is initialized and view is attached + init(/* useMigrationFlag= */ true) + reset(mLockIconView) + + // WHEN the dozing state changes + mUnderTest.mIsDozingCallback.accept(true) + + // THEN the AOD lock icon should show + verify(mLockIconView).updateIcon(ICON_LOCK, true) + } + + /** After migration, replaces LockIconViewControllerTest version */ + @Test + fun testBurnInOffsetsUpdated_onDozeAmountChanged() = + runBlocking(IMMEDIATE) { + // GIVEN udfps enrolled + setupUdfps() + whenever(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(true) + + // GIVEN burn-in offset = 5 + val burnInOffset = 5 + whenever(getBurnInOffset(anyInt(), anyBoolean())).thenReturn(burnInOffset) + + // GIVEN starting state for the lock icon (keyguard) + setupShowLockIcon() + init(/* useMigrationFlag= */ true) + reset(mLockIconView) + + // WHEN dozing updates + mUnderTest.mIsDozingCallback.accept(true) + mUnderTest.mDozeTransitionCallback.accept(TransitionStep(LOCKSCREEN, AOD, 1f, FINISHED)) + + // THEN the view's translation is updated to use the AoD burn-in offsets + verify(mLockIconView).setTranslationY(burnInOffset.toFloat()) + verify(mLockIconView).setTranslationX(burnInOffset.toFloat()) + reset(mLockIconView) + + // WHEN the device is no longer dozing + mUnderTest.mIsDozingCallback.accept(false) + mUnderTest.mDozeTransitionCallback.accept(TransitionStep(AOD, LOCKSCREEN, 0f, FINISHED)) + + // THEN the view is updated to NO translation (no burn-in offsets anymore) + verify(mLockIconView).setTranslationY(0f) + verify(mLockIconView).setTranslationX(0f) + } + + companion object { + private val IMMEDIATE = Dispatchers.Main.immediate + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuControllerTest.java index 19a6c66652dd..77d38c58e685 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuControllerTest.java @@ -35,6 +35,7 @@ import android.provider.Settings; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.view.WindowManager; +import android.view.accessibility.AccessibilityManager; import androidx.test.filters.SmallTest; @@ -68,6 +69,7 @@ public class AccessibilityFloatingMenuControllerTest extends SysuiTestCase { public MockitoRule mockito = MockitoJUnit.rule(); private Context mContextWrapper; + private AccessibilityManager mAccessibilityManager; private KeyguardUpdateMonitor mKeyguardUpdateMonitor; private AccessibilityFloatingMenuController mController; private AccessibilityButtonTargetsObserver mTargetsObserver; @@ -87,6 +89,7 @@ public class AccessibilityFloatingMenuControllerTest extends SysuiTestCase { } }; + mAccessibilityManager = mContext.getSystemService(AccessibilityManager.class); mLastButtonTargets = Settings.Secure.getStringForUser(mContextWrapper.getContentResolver(), Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, UserHandle.USER_CURRENT); mLastButtonMode = Settings.Secure.getIntForUser(mContextWrapper.getContentResolver(), @@ -348,8 +351,8 @@ public class AccessibilityFloatingMenuControllerTest extends SysuiTestCase { mKeyguardUpdateMonitor = Dependency.get(KeyguardUpdateMonitor.class); final AccessibilityFloatingMenuController controller = new AccessibilityFloatingMenuController(mContextWrapper, windowManager, - displayManager, mTargetsObserver, mModeObserver, mKeyguardUpdateMonitor, - featureFlags); + displayManager, mAccessibilityManager, mTargetsObserver, mModeObserver, + mKeyguardUpdateMonitor, featureFlags); controller.init(); return controller; diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationControllerTest.java new file mode 100644 index 000000000000..8ef65dcb2c3a --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationControllerTest.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.accessibility.floatingmenu; + +import static org.mockito.Mockito.verify; + +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.view.WindowManager; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; +import com.android.wm.shell.bubbles.DismissView; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for {@link DismissAnimationController}. */ +@SmallTest +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper +public class DismissAnimationControllerTest extends SysuiTestCase { + private DismissAnimationController mDismissAnimationController; + private DismissView mDismissView; + + @Before + public void setUp() throws Exception { + final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class); + final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext); + final MenuViewAppearance stubMenuViewAppearance = new MenuViewAppearance(mContext, + stubWindowManager); + final MenuView stubMenuView = new MenuView(mContext, stubMenuViewModel, + stubMenuViewAppearance); + mDismissView = new DismissView(mContext); + mDismissAnimationController = new DismissAnimationController(mDismissView, stubMenuView); + } + + @Test + public void showDismissView_success() { + mDismissAnimationController.showDismissView(true); + + verify(mDismissView).show(); + } + + @Test + public void hideDismissView_success() { + mDismissAnimationController.showDismissView(false); + + verify(mDismissView).hide(); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java index dbf291c49ee5..d0bd4f7026eb 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java @@ -18,9 +18,16 @@ package com.android.systemui.accessibility.floatingmenu; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + import android.graphics.PointF; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; +import android.view.View; +import android.view.ViewPropertyAnimator; import android.view.WindowManager; import androidx.test.filters.SmallTest; @@ -36,6 +43,8 @@ import org.junit.runner.RunWith; @TestableLooper.RunWithLooper(setAsMainLooper = true) @SmallTest public class MenuAnimationControllerTest extends SysuiTestCase { + + private ViewPropertyAnimator mViewPropertyAnimator; private MenuView mMenuView; private MenuAnimationController mMenuAnimationController; @@ -45,7 +54,11 @@ public class MenuAnimationControllerTest extends SysuiTestCase { final MenuViewAppearance stubMenuViewAppearance = new MenuViewAppearance(mContext, stubWindowManager); final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext); - mMenuView = new MenuView(mContext, stubMenuViewModel, stubMenuViewAppearance); + + mMenuView = spy(new MenuView(mContext, stubMenuViewModel, stubMenuViewAppearance)); + mViewPropertyAnimator = spy(mMenuView.animate()); + doReturn(mViewPropertyAnimator).when(mMenuView).animate(); + mMenuAnimationController = new MenuAnimationController(mMenuView); } @@ -58,4 +71,20 @@ public class MenuAnimationControllerTest extends SysuiTestCase { assertThat(mMenuView.getTranslationX()).isEqualTo(50); assertThat(mMenuView.getTranslationY()).isEqualTo(60); } + + @Test + public void startShrinkAnimation_verifyAnimationEndAction() { + mMenuAnimationController.startShrinkAnimation(() -> mMenuView.setVisibility(View.VISIBLE)); + + verify(mViewPropertyAnimator).withEndAction(any(Runnable.class)); + } + + @Test + public void startGrowAnimation_menuCompletelyOpaque() { + mMenuAnimationController.startShrinkAnimation(null); + + mMenuAnimationController.startGrowAnimation(); + + assertThat(mMenuView.getAlpha()).isEqualTo(/* completelyOpaque */ 1.0f); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java index bf6d574a0f67..78ee627a9a2f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java @@ -43,6 +43,7 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; @@ -54,6 +55,9 @@ public class MenuItemAccessibilityDelegateTest extends SysuiTestCase { @Rule public MockitoRule mockito = MockitoJUnit.rule(); + @Mock + private DismissAnimationController.DismissCallback mStubDismissCallback; + private RecyclerView mStubListView; private MenuView mMenuView; private MenuItemAccessibilityDelegate mMenuItemAccessibilityDelegate; @@ -87,7 +91,7 @@ public class MenuItemAccessibilityDelegateTest extends SysuiTestCase { mMenuItemAccessibilityDelegate.onInitializeAccessibilityNodeInfo(mStubListView, info); - assertThat(info.getActionList().size()).isEqualTo(5); + assertThat(info.getActionList().size()).isEqualTo(6); } @Test @@ -156,6 +160,17 @@ public class MenuItemAccessibilityDelegateTest extends SysuiTestCase { } @Test + public void performRemoveMenuAction_success() { + mMenuAnimationController.setDismissCallback(mStubDismissCallback); + final boolean removeMenuAction = + mMenuItemAccessibilityDelegate.performAccessibilityAction(mStubListView, + R.id.action_remove_menu, null); + + assertThat(removeMenuAction).isTrue(); + verify(mMenuAnimationController).removeMenu(); + } + + @Test public void performFocusAction_fadeIn() { mMenuItemAccessibilityDelegate.performAccessibilityAction(mStubListView, ACTION_ACCESSIBILITY_FOCUS, null); diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java index c5b9a294fc34..4acb394bee95 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java @@ -21,6 +21,8 @@ import static android.view.View.OVER_SCROLL_NEVER; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.anyFloat; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; @@ -36,6 +38,7 @@ import androidx.test.filters.SmallTest; import com.android.internal.accessibility.dialog.AccessibilityTarget; import com.android.systemui.SysuiTestCase; import com.android.systemui.accessibility.MotionEventHelper; +import com.android.wm.shell.bubbles.DismissView; import org.junit.After; import org.junit.Before; @@ -57,7 +60,9 @@ public class MenuListViewTouchHandlerTest extends SysuiTestCase { private MenuView mStubMenuView; private MenuListViewTouchHandler mTouchHandler; private MenuAnimationController mMenuAnimationController; + private DismissAnimationController mDismissAnimationController; private RecyclerView mStubListView; + private DismissView mDismissView; @Before public void setUp() throws Exception { @@ -69,7 +74,11 @@ public class MenuListViewTouchHandlerTest extends SysuiTestCase { mStubMenuView.setTranslationX(0); mStubMenuView.setTranslationY(0); mMenuAnimationController = spy(new MenuAnimationController(mStubMenuView)); - mTouchHandler = new MenuListViewTouchHandler(mMenuAnimationController); + mDismissView = spy(new DismissView(mContext)); + mDismissAnimationController = + spy(new DismissAnimationController(mDismissView, mStubMenuView)); + mTouchHandler = new MenuListViewTouchHandler(mMenuAnimationController, + mDismissAnimationController); final AccessibilityTargetAdapter stubAdapter = new AccessibilityTargetAdapter(mStubTargets); mStubListView = (RecyclerView) mStubMenuView.getChildAt(0); mStubListView.setAdapter(stubAdapter); @@ -88,7 +97,9 @@ public class MenuListViewTouchHandlerTest extends SysuiTestCase { } @Test - public void onActionMoveEvent_shouldMoveToPosition() { + public void onActionMoveEvent_notConsumedEvent_shouldMoveToPosition() { + doReturn(false).when(mDismissAnimationController).maybeConsumeMoveMotionEvent( + any(MotionEvent.class)); final int offset = 100; final MotionEvent stubDownEvent = mMotionEventHelper.obtainMotionEvent(/* downTime= */ 0, /* eventTime= */ 1, @@ -108,6 +119,24 @@ public class MenuListViewTouchHandlerTest extends SysuiTestCase { } @Test + public void onActionMoveEvent_shouldShowDismissView() { + final int offset = 100; + final MotionEvent stubDownEvent = + mMotionEventHelper.obtainMotionEvent(/* downTime= */ 0, /* eventTime= */ 1, + MotionEvent.ACTION_DOWN, mStubMenuView.getTranslationX(), + mStubMenuView.getTranslationY()); + final MotionEvent stubMoveEvent = + mMotionEventHelper.obtainMotionEvent(/* downTime= */ 0, /* eventTime= */ 3, + MotionEvent.ACTION_MOVE, mStubMenuView.getTranslationX() + offset, + mStubMenuView.getTranslationY() + offset); + + mTouchHandler.onInterceptTouchEvent(mStubListView, stubDownEvent); + mTouchHandler.onInterceptTouchEvent(mStubListView, stubMoveEvent); + + verify(mDismissView).show(); + } + + @Test public void dragAndDrop_shouldFlingMenuThenSpringToEdge() { final int offset = 100; final MotionEvent stubDownEvent = diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerControllerTest.java index 8c8d6aca7cd7..dd7ce0e06c32 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerControllerTest.java @@ -34,6 +34,7 @@ import android.view.ViewGroup; import android.view.WindowInsets; import android.view.WindowManager; import android.view.WindowMetrics; +import android.view.accessibility.AccessibilityManager; import androidx.test.filters.SmallTest; @@ -59,6 +60,9 @@ public class MenuViewLayerControllerTest extends SysuiTestCase { private WindowManager mWindowManager; @Mock + private AccessibilityManager mAccessibilityManager; + + @Mock private WindowMetrics mWindowMetrics; private MenuViewLayerController mMenuViewLayerController; @@ -72,7 +76,8 @@ public class MenuViewLayerControllerTest extends SysuiTestCase { when(mWindowManager.getCurrentWindowMetrics()).thenReturn(mWindowMetrics); when(mWindowMetrics.getBounds()).thenReturn(new Rect(0, 0, 1080, 2340)); when(mWindowMetrics.getWindowInsets()).thenReturn(stubDisplayInsets()); - mMenuViewLayerController = new MenuViewLayerController(mContext, mWindowManager); + mMenuViewLayerController = new MenuViewLayerController(mContext, mWindowManager, + mAccessibilityManager); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java index 23c6ef1338b3..d20eeafde09c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java @@ -23,18 +23,25 @@ import static com.android.systemui.accessibility.floatingmenu.MenuViewLayer.Laye import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.verify; + import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.view.View; import android.view.WindowManager; +import android.view.accessibility.AccessibilityManager; import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; /** Tests for {@link MenuViewLayer}. */ @RunWith(AndroidTestingRunner.class) @@ -43,10 +50,19 @@ import org.junit.runner.RunWith; public class MenuViewLayerTest extends SysuiTestCase { private MenuViewLayer mMenuViewLayer; + @Rule + public MockitoRule mockito = MockitoJUnit.rule(); + + @Mock + private IAccessibilityFloatingMenu mFloatingMenu; + @Before public void setUp() throws Exception { final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class); - mMenuViewLayer = new MenuViewLayer(mContext, stubWindowManager); + final AccessibilityManager stubAccessibilityManager = mContext.getSystemService( + AccessibilityManager.class); + mMenuViewLayer = new MenuViewLayer(mContext, stubWindowManager, stubAccessibilityManager, + mFloatingMenu); } @Test @@ -64,4 +80,11 @@ public class MenuViewLayerTest extends SysuiTestCase { assertThat(menuView.getVisibility()).isEqualTo(GONE); } + + @Test + public void tiggerDismissMenuAction_hideFloatingMenu() { + mMenuViewLayer.mDismissMenuAction.run(); + + verify(mFloatingMenu).hide(); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt index d1107c612977..45b8ce1ce247 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt @@ -41,10 +41,15 @@ import com.android.internal.jank.InteractionJankMonitor import com.android.internal.widget.LockPatternUtils import com.android.systemui.R import com.android.systemui.SysuiTestCase +import com.android.systemui.biometrics.data.repository.FakePromptRepository +import com.android.systemui.biometrics.domain.interactor.FakeCredentialInteractor +import com.android.systemui.biometrics.domain.interactor.BiometricPromptCredentialInteractor +import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel import com.android.systemui.keyguard.WakefulnessLifecycle import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.Dispatchers import org.junit.After import org.junit.Rule import org.junit.Test @@ -80,6 +85,15 @@ class AuthContainerViewTest : SysuiTestCase() { @Mock lateinit var interactionJankMonitor: InteractionJankMonitor + private val biometricPromptRepository = FakePromptRepository() + private val credentialInteractor = FakeCredentialInteractor() + private val bpCredentialInteractor = BiometricPromptCredentialInteractor( + Dispatchers.Main.immediate, + biometricPromptRepository, + credentialInteractor + ) + private val credentialViewModel = CredentialViewModel(mContext, bpCredentialInteractor) + private var authContainer: TestAuthContainerView? = null @After @@ -466,6 +480,8 @@ class AuthContainerViewTest : SysuiTestCase() { userManager, lockPatternUtils, interactionJankMonitor, + { bpCredentialInteractor }, + { credentialViewModel }, Handler(TestableLooper.get(this).looper), FakeExecutor(FakeSystemClock()) ) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java index 8e45067c8e13..4dd46edd0912 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java @@ -25,7 +25,9 @@ import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_AWA import static com.google.common.truth.Truth.assertThat; import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertNull; +import static junit.framework.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; @@ -87,6 +89,8 @@ import com.android.internal.R; import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.widget.LockPatternUtils; import com.android.systemui.SysuiTestCase; +import com.android.systemui.biometrics.domain.interactor.BiometricPromptCredentialInteractor; +import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.CommandQueue; @@ -163,6 +167,11 @@ public class AuthControllerTest extends SysuiTestCase { private UdfpsLogger mUdfpsLogger; @Mock private InteractionJankMonitor mInteractionJankMonitor; + @Mock + private BiometricPromptCredentialInteractor mBiometricPromptCredentialInteractor; + @Mock + private CredentialViewModel mCredentialViewModel; + @Captor private ArgumentCaptor<IFingerprintAuthenticatorsRegisteredCallback> mFpAuthenticatorsRegisteredCaptor; @Captor @@ -236,7 +245,7 @@ public class AuthControllerTest extends SysuiTestCase { 2 /* sensorId */, SensorProperties.STRENGTH_STRONG, 1 /* maxEnrollmentsPerUser */, - fpComponentInfo, + faceComponentInfo, FaceSensorProperties.TYPE_RGB, true /* supportsFaceDetection */, true /* supportsSelfIllumination */, @@ -276,8 +285,6 @@ public class AuthControllerTest extends SysuiTestCase { reset(mFingerprintManager); reset(mFaceManager); - when(mVibratorHelper.hasVibrator()).thenReturn(true); - // This test requires an uninitialized AuthController. AuthController authController = new TestableAuthController(mContextSpy, mExecution, mCommandQueue, mActivityTaskManager, mWindowManager, mFingerprintManager, @@ -308,8 +315,6 @@ public class AuthControllerTest extends SysuiTestCase { reset(mFingerprintManager); reset(mFaceManager); - when(mVibratorHelper.hasVibrator()).thenReturn(true); - // This test requires an uninitialized AuthController. AuthController authController = new TestableAuthController(mContextSpy, mExecution, mCommandQueue, mActivityTaskManager, mWindowManager, mFingerprintManager, @@ -343,6 +348,36 @@ public class AuthControllerTest extends SysuiTestCase { } @Test + public void testFaceAuthEnrollmentStatus() throws RemoteException { + final int userId = 0; + + reset(mFaceManager); + mAuthController.start(); + + verify(mFaceManager).addAuthenticatorsRegisteredCallback( + mFaceAuthenticatorsRegisteredCaptor.capture()); + + mFaceAuthenticatorsRegisteredCaptor.getValue().onAllAuthenticatorsRegistered( + mFaceManager.getSensorPropertiesInternal()); + mTestableLooper.processAllMessages(); + + verify(mFaceManager).registerBiometricStateListener( + mBiometricStateCaptor.capture()); + + assertFalse(mAuthController.isFaceAuthEnrolled(userId)); + + // Enrollments changed for an unknown sensor. + for (BiometricStateListener listener : mBiometricStateCaptor.getAllValues()) { + listener.onEnrollmentsChanged(userId, + 2 /* sensorId */, true /* hasEnrollments */); + } + mTestableLooper.processAllMessages(); + + assertTrue(mAuthController.isFaceAuthEnrolled(userId)); + } + + + @Test public void testSendsReasonUserCanceled_whenDismissedByUserCancel() throws Exception { showDialog(new int[]{1} /* sensorIds */, false /* credentialAllowed */); mAuthController.onDismissed(AuthDialogCallback.DISMISSED_USER_CANCELED, @@ -981,6 +1016,7 @@ public class AuthControllerTest extends SysuiTestCase { fingerprintManager, faceManager, udfpsControllerFactory, sidefpsControllerFactory, mDisplayManager, mWakefulnessLifecycle, mUserManager, mLockPatternUtils, mUdfpsLogger, statusBarStateController, + () -> mBiometricPromptCredentialInteractor, () -> mCredentialViewModel, mInteractionJankMonitor, mHandler, mBackgroundExecutor, vibratorHelper); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt index 8820c164cba4..1379a0eeebdd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt @@ -22,12 +22,11 @@ import android.hardware.biometrics.BiometricManager import android.hardware.biometrics.ComponentInfoInternal import android.hardware.biometrics.PromptInfo import android.hardware.biometrics.SensorProperties -import android.hardware.face.FaceSensorPropertiesInternal import android.hardware.face.FaceSensorProperties +import android.hardware.face.FaceSensorPropertiesInternal import android.hardware.fingerprint.FingerprintSensorProperties import android.hardware.fingerprint.FingerprintSensorPropertiesInternal import android.os.Bundle - import android.testing.ViewUtils import android.view.LayoutInflater @@ -83,26 +82,31 @@ internal fun AuthBiometricView?.destroyDialog() { internal fun fingerprintSensorPropertiesInternal( ids: List<Int> = listOf(0) ): List<FingerprintSensorPropertiesInternal> { - val componentInfo = listOf( + val componentInfo = + listOf( ComponentInfoInternal( - "fingerprintSensor" /* componentId */, - "vendor/model/revision" /* hardwareVersion */, "1.01" /* firmwareVersion */, - "00000001" /* serialNumber */, "" /* softwareVersion */ + "fingerprintSensor" /* componentId */, + "vendor/model/revision" /* hardwareVersion */, + "1.01" /* firmwareVersion */, + "00000001" /* serialNumber */, + "" /* softwareVersion */ ), ComponentInfoInternal( - "matchingAlgorithm" /* componentId */, - "" /* hardwareVersion */, "" /* firmwareVersion */, "" /* serialNumber */, - "vendor/version/revision" /* softwareVersion */ + "matchingAlgorithm" /* componentId */, + "" /* hardwareVersion */, + "" /* firmwareVersion */, + "" /* serialNumber */, + "vendor/version/revision" /* softwareVersion */ ) - ) + ) return ids.map { id -> FingerprintSensorPropertiesInternal( - id, - SensorProperties.STRENGTH_STRONG, - 5 /* maxEnrollmentsPerUser */, - componentInfo, - FingerprintSensorProperties.TYPE_REAR, - false /* resetLockoutRequiresHardwareAuthToken */ + id, + SensorProperties.STRENGTH_STRONG, + 5 /* maxEnrollmentsPerUser */, + componentInfo, + FingerprintSensorProperties.TYPE_REAR, + false /* resetLockoutRequiresHardwareAuthToken */ ) } } @@ -111,28 +115,53 @@ internal fun fingerprintSensorPropertiesInternal( internal fun faceSensorPropertiesInternal( ids: List<Int> = listOf(1) ): List<FaceSensorPropertiesInternal> { - val componentInfo = listOf( + val componentInfo = + listOf( ComponentInfoInternal( - "faceSensor" /* componentId */, - "vendor/model/revision" /* hardwareVersion */, "1.01" /* firmwareVersion */, - "00000001" /* serialNumber */, "" /* softwareVersion */ + "faceSensor" /* componentId */, + "vendor/model/revision" /* hardwareVersion */, + "1.01" /* firmwareVersion */, + "00000001" /* serialNumber */, + "" /* softwareVersion */ ), ComponentInfoInternal( - "matchingAlgorithm" /* componentId */, - "" /* hardwareVersion */, "" /* firmwareVersion */, "" /* serialNumber */, - "vendor/version/revision" /* softwareVersion */ + "matchingAlgorithm" /* componentId */, + "" /* hardwareVersion */, + "" /* firmwareVersion */, + "" /* serialNumber */, + "vendor/version/revision" /* softwareVersion */ ) - ) + ) return ids.map { id -> FaceSensorPropertiesInternal( - id, - SensorProperties.STRENGTH_STRONG, - 2 /* maxEnrollmentsPerUser */, - componentInfo, - FaceSensorProperties.TYPE_RGB, - true /* supportsFaceDetection */, - true /* supportsSelfIllumination */, - false /* resetLockoutRequiresHardwareAuthToken */ + id, + SensorProperties.STRENGTH_STRONG, + 2 /* maxEnrollmentsPerUser */, + componentInfo, + FaceSensorProperties.TYPE_RGB, + true /* supportsFaceDetection */, + true /* supportsSelfIllumination */, + false /* resetLockoutRequiresHardwareAuthToken */ ) } } + +internal fun promptInfo( + title: String = "title", + subtitle: String = "sub", + description: String = "desc", + credentialTitle: String? = "cred title", + credentialSubtitle: String? = "cred sub", + credentialDescription: String? = "cred desc", + negativeButton: String = "neg", +): PromptInfo { + val info = PromptInfo() + info.title = title + info.subtitle = subtitle + info.description = description + credentialTitle?.let { info.deviceCredentialTitle = it } + credentialSubtitle?.let { info.deviceCredentialSubtitle = it } + credentialDescription?.let { info.deviceCredentialDescription = it } + info.negativeButtonText = negativeButton + return info +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java index 49c6fd14997e..ed957db2852b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java @@ -69,6 +69,7 @@ import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.animation.ActivityLaunchAnimator; import com.android.systemui.dump.DumpManager; +import com.android.systemui.flags.FeatureFlags; import com.android.systemui.keyguard.ScreenLifecycle; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; @@ -169,6 +170,8 @@ public class UdfpsControllerTest extends SysuiTestCase { private FakeExecutor mFgExecutor; @Mock private UdfpsDisplayMode mUdfpsDisplayMode; + @Mock + private FeatureFlags mFeatureFlags; // Stuff for configuring mocks @Mock @@ -250,6 +253,7 @@ public class UdfpsControllerTest extends SysuiTestCase { mStatusBarKeyguardViewManager, mDumpManager, mKeyguardUpdateMonitor, + mFeatureFlags, mFalsingManager, mPowerManager, mAccessibilityManager, diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/PromptRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/PromptRepositoryImplTest.kt new file mode 100644 index 000000000000..2d5614c15173 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/PromptRepositoryImplTest.kt @@ -0,0 +1,81 @@ +package com.android.systemui.biometrics.data.repository + +import android.hardware.biometrics.PromptInfo +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.biometrics.AuthController +import com.android.systemui.biometrics.data.model.PromptKind +import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.mockito.withArgCaptor +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runBlockingTest +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.eq +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.junit.MockitoJUnit + +@SmallTest +@RunWith(JUnit4::class) +class PromptRepositoryImplTest : SysuiTestCase() { + + @JvmField @Rule var mockitoRule = MockitoJUnit.rule() + + @Mock private lateinit var authController: AuthController + + private lateinit var repository: PromptRepositoryImpl + + @Before + fun setup() { + repository = PromptRepositoryImpl(authController) + } + + @Test + fun isShowing() = runBlockingTest { + whenever(authController.isShowing).thenReturn(true) + + val values = mutableListOf<Boolean>() + val job = launch { repository.isShowing.toList(values) } + assertThat(values).containsExactly(true) + + withArgCaptor<AuthController.Callback> { + verify(authController).addCallback(capture()) + + value.onBiometricPromptShown() + assertThat(values).containsExactly(true, true) + + value.onBiometricPromptDismissed() + assertThat(values).containsExactly(true, true, false).inOrder() + + job.cancel() + verify(authController).removeCallback(eq(value)) + } + } + + @Test + fun setsAndUnsetsPrompt() = runBlockingTest { + val kind = PromptKind.PIN + val uid = 8 + val challenge = 90L + val promptInfo = PromptInfo() + + repository.setPrompt(promptInfo, uid, challenge, kind) + + assertThat(repository.kind.value).isEqualTo(kind) + assertThat(repository.userId.value).isEqualTo(uid) + assertThat(repository.challenge.value).isEqualTo(challenge) + assertThat(repository.promptInfo.value).isSameInstanceAs(promptInfo) + + repository.unsetPrompt() + + assertThat(repository.promptInfo.value).isNull() + assertThat(repository.userId.value).isNull() + assertThat(repository.challenge.value).isNull() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractorImplTest.kt new file mode 100644 index 000000000000..97d3e688ed80 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractorImplTest.kt @@ -0,0 +1,216 @@ +package com.android.systemui.biometrics.domain.interactor + +import android.app.admin.DevicePolicyManager +import android.app.admin.DevicePolicyResourcesManager +import android.content.pm.UserInfo +import android.os.UserManager +import androidx.test.filters.SmallTest +import com.android.internal.widget.LockPatternUtils +import com.android.internal.widget.LockscreenCredential +import com.android.internal.widget.VerifyCredentialResponse +import com.android.systemui.SysuiTestCase +import com.android.systemui.biometrics.domain.model.BiometricOperationInfo +import com.android.systemui.biometrics.domain.model.BiometricPromptRequest +import com.android.systemui.biometrics.domain.model.BiometricUserInfo +import com.android.systemui.biometrics.promptInfo +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.time.FakeSystemClock +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.toList +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.anyInt +import org.mockito.ArgumentMatchers.anyLong +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.junit.MockitoJUnit + +private const val USER_ID = 22 +private const val OPERATION_ID = 100L +private const val MAX_ATTEMPTS = 5 + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(JUnit4::class) +class CredentialInteractorImplTest : SysuiTestCase() { + + @JvmField @Rule var mockitoRule = MockitoJUnit.rule() + + @Mock private lateinit var lockPatternUtils: LockPatternUtils + @Mock private lateinit var userManager: UserManager + @Mock private lateinit var devicePolicyManager: DevicePolicyManager + @Mock private lateinit var devicePolicyResourcesManager: DevicePolicyResourcesManager + + private val systemClock = FakeSystemClock() + + private lateinit var interactor: CredentialInteractorImpl + + @Before + fun setup() { + whenever(devicePolicyManager.resources).thenReturn(devicePolicyResourcesManager) + whenever(lockPatternUtils.getMaximumFailedPasswordsForWipe(anyInt())) + .thenReturn(MAX_ATTEMPTS) + whenever(userManager.getUserInfo(eq(USER_ID))).thenReturn(UserInfo(USER_ID, "", 0)) + whenever(devicePolicyManager.getProfileWithMinimumFailedPasswordsForWipe(eq(USER_ID))) + .thenReturn(USER_ID) + + interactor = + CredentialInteractorImpl( + mContext, + lockPatternUtils, + userManager, + devicePolicyManager, + systemClock + ) + } + + @Test + fun testStealthMode() { + for (value in listOf(true, false, false, true)) { + whenever(lockPatternUtils.isVisiblePatternEnabled(eq(USER_ID))).thenReturn(value) + + assertThat(interactor.isStealthModeActive(USER_ID)).isEqualTo(!value) + } + } + + @Test + fun testCredentialOwner() { + for (value in listOf(12, 8, 4)) { + whenever(userManager.getCredentialOwnerProfile(eq(USER_ID))).thenReturn(value) + + assertThat(interactor.getCredentialOwnerOrSelfId(USER_ID)).isEqualTo(value) + } + } + + @Test fun pinCredentialWhenGood() = pinCredential(goodCredential()) + + @Test fun pinCredentialWhenBad() = pinCredential(badCredential()) + + @Test fun pinCredentialWhenBadAndThrottled() = pinCredential(badCredential(timeout = 5_000)) + + private fun pinCredential(result: VerifyCredentialResponse) = runTest { + val usedAttempts = 1 + whenever(lockPatternUtils.getCurrentFailedPasswordAttempts(eq(USER_ID))) + .thenReturn(usedAttempts) + whenever(lockPatternUtils.verifyCredential(any(), eq(USER_ID), anyInt())).thenReturn(result) + whenever(lockPatternUtils.verifyGatekeeperPasswordHandle(anyLong(), anyLong(), eq(USER_ID))) + .thenReturn(result) + whenever(lockPatternUtils.setLockoutAttemptDeadline(anyInt(), anyInt())).thenAnswer { + systemClock.elapsedRealtime() + (it.arguments[1] as Int) + } + + // wrap in an async block so the test can advance the clock if throttling credential + // checks prevents the method from returning + val statusList = mutableListOf<CredentialStatus>() + interactor + .verifyCredential(pinRequest(), LockscreenCredential.createPin("1234")) + .toList(statusList) + + val last = statusList.removeLastOrNull() + if (result.isMatched) { + assertThat(statusList).isEmpty() + val successfulResult = last as? CredentialStatus.Success.Verified + assertThat(successfulResult).isNotNull() + assertThat(successfulResult!!.hat).isEqualTo(result.gatekeeperHAT) + + verify(lockPatternUtils).userPresent(eq(USER_ID)) + verify(lockPatternUtils) + .removeGatekeeperPasswordHandle(eq(result.gatekeeperPasswordHandle)) + } else { + val failedResult = last as? CredentialStatus.Fail.Error + assertThat(failedResult).isNotNull() + assertThat(failedResult!!.remainingAttempts) + .isEqualTo(if (result.timeout > 0) null else MAX_ATTEMPTS - usedAttempts - 1) + assertThat(failedResult.urgentMessage).isNull() + + if (result.timeout > 0) { // failed and throttled + // messages are in the throttled errors, so the final Error.error is empty + assertThat(failedResult.error).isEmpty() + assertThat(statusList).isNotEmpty() + assertThat(statusList.filterIsInstance(CredentialStatus.Fail.Throttled::class.java)) + .hasSize(statusList.size) + + verify(lockPatternUtils).setLockoutAttemptDeadline(eq(USER_ID), eq(result.timeout)) + } else { // failed + assertThat(failedResult.error) + .matches(Regex("(.*)try again(.*)", RegexOption.IGNORE_CASE).toPattern()) + assertThat(statusList).isEmpty() + + verify(lockPatternUtils).reportFailedPasswordAttempt(eq(USER_ID)) + } + } + } + + @Test + fun pinCredentialWhenBadAndFinalAttempt() = runTest { + whenever(lockPatternUtils.verifyCredential(any(), eq(USER_ID), anyInt())) + .thenReturn(badCredential()) + whenever(lockPatternUtils.getCurrentFailedPasswordAttempts(eq(USER_ID))) + .thenReturn(MAX_ATTEMPTS - 2) + + val statusList = mutableListOf<CredentialStatus>() + interactor + .verifyCredential(pinRequest(), LockscreenCredential.createPin("1234")) + .toList(statusList) + + val result = statusList.removeLastOrNull() as? CredentialStatus.Fail.Error + assertThat(result).isNotNull() + assertThat(result!!.remainingAttempts).isEqualTo(1) + assertThat(result.urgentMessage).isNotEmpty() + assertThat(statusList).isEmpty() + + verify(lockPatternUtils).reportFailedPasswordAttempt(eq(USER_ID)) + } + + @Test + fun pinCredentialWhenBadAndNoMoreAttempts() = runTest { + whenever(lockPatternUtils.verifyCredential(any(), eq(USER_ID), anyInt())) + .thenReturn(badCredential()) + whenever(lockPatternUtils.getCurrentFailedPasswordAttempts(eq(USER_ID))) + .thenReturn(MAX_ATTEMPTS - 1) + whenever(devicePolicyResourcesManager.getString(any(), any())).thenReturn("wipe") + + val statusList = mutableListOf<CredentialStatus>() + interactor + .verifyCredential(pinRequest(), LockscreenCredential.createPin("1234")) + .toList(statusList) + + val result = statusList.removeLastOrNull() as? CredentialStatus.Fail.Error + assertThat(result).isNotNull() + assertThat(result!!.remainingAttempts).isEqualTo(0) + assertThat(result.urgentMessage).isNotEmpty() + assertThat(statusList).isEmpty() + + verify(lockPatternUtils).reportFailedPasswordAttempt(eq(USER_ID)) + } +} + +private fun pinRequest(): BiometricPromptRequest.Credential.Pin = + BiometricPromptRequest.Credential.Pin( + promptInfo(), + BiometricUserInfo(USER_ID), + BiometricOperationInfo(OPERATION_ID) + ) + +private fun goodCredential( + passwordHandle: Long = 90, + hat: ByteArray = ByteArray(69), +): VerifyCredentialResponse = + VerifyCredentialResponse.Builder() + .setGatekeeperPasswordHandle(passwordHandle) + .setGatekeeperHAT(hat) + .build() + +private fun badCredential(timeout: Int = 0): VerifyCredentialResponse = + if (timeout > 0) { + VerifyCredentialResponse.fromTimeout(timeout) + } else { + VerifyCredentialResponse.fromError() + } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractorTest.kt new file mode 100644 index 000000000000..dbcbf415221e --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractorTest.kt @@ -0,0 +1,270 @@ +package com.android.systemui.biometrics.domain.interactor + +import android.hardware.biometrics.PromptInfo +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.biometrics.Utils +import com.android.systemui.biometrics.data.repository.FakePromptRepository +import com.android.systemui.biometrics.domain.model.BiometricOperationInfo +import com.android.systemui.biometrics.domain.model.BiometricPromptRequest +import com.android.systemui.biometrics.domain.model.BiometricUserInfo +import com.android.systemui.biometrics.promptInfo +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.UnconfinedTestDispatcher +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.junit.MockitoJUnit + +private const val USER_ID = 22 +private const val OPERATION_ID = 100L + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(JUnit4::class) +class PromptCredentialInteractorTest : SysuiTestCase() { + + @JvmField @Rule var mockitoRule = MockitoJUnit.rule() + + private val dispatcher = UnconfinedTestDispatcher() + private val biometricPromptRepository = FakePromptRepository() + private val credentialInteractor = FakeCredentialInteractor() + + private lateinit var interactor: BiometricPromptCredentialInteractor + + @Before + fun setup() { + interactor = + BiometricPromptCredentialInteractor( + dispatcher, + biometricPromptRepository, + credentialInteractor + ) + } + + @Test + fun testIsShowing() = + runTest(dispatcher) { + var showing = false + val job = launch { interactor.isShowing.collect { showing = it } } + + biometricPromptRepository.setIsShowing(false) + assertThat(showing).isFalse() + + biometricPromptRepository.setIsShowing(true) + assertThat(showing).isTrue() + + job.cancel() + } + + @Test + fun testShowError() = + runTest(dispatcher) { + var error: CredentialStatus.Fail? = null + val job = launch { interactor.verificationError.collect { error = it } } + + for (msg in listOf("once", "again")) { + interactor.setVerificationError(error(msg)) + assertThat(error).isEqualTo(error(msg)) + } + + interactor.resetVerificationError() + assertThat(error).isNull() + + job.cancel() + } + + @Test + fun nullWhenNoPromptInfo() = + runTest(dispatcher) { + var prompt: BiometricPromptRequest? = null + val job = launch { interactor.prompt.collect { prompt = it } } + + assertThat(prompt).isNull() + + job.cancel() + } + + @Test fun usePinCredentialForPrompt() = useCredentialForPrompt(Utils.CREDENTIAL_PIN) + + @Test fun usePasswordCredentialForPrompt() = useCredentialForPrompt(Utils.CREDENTIAL_PASSWORD) + + @Test fun usePatternCredentialForPrompt() = useCredentialForPrompt(Utils.CREDENTIAL_PATTERN) + + private fun useCredentialForPrompt(kind: Int) = + runTest(dispatcher) { + val isStealth = false + credentialInteractor.stealthMode = isStealth + + var prompt: BiometricPromptRequest? = null + val job = launch { interactor.prompt.collect { prompt = it } } + + val title = "what a prompt" + val subtitle = "s" + val description = "something to see" + + interactor.useCredentialsForAuthentication( + PromptInfo().also { + it.title = title + it.description = description + it.subtitle = subtitle + }, + kind = kind, + userId = USER_ID, + challenge = OPERATION_ID + ) + + val p = prompt as? BiometricPromptRequest.Credential + assertThat(p).isNotNull() + assertThat(p!!.title).isEqualTo(title) + assertThat(p.subtitle).isEqualTo(subtitle) + assertThat(p.description).isEqualTo(description) + assertThat(p.userInfo).isEqualTo(BiometricUserInfo(USER_ID)) + assertThat(p.operationInfo).isEqualTo(BiometricOperationInfo(OPERATION_ID)) + assertThat(p) + .isInstanceOf( + when (kind) { + Utils.CREDENTIAL_PIN -> BiometricPromptRequest.Credential.Pin::class.java + Utils.CREDENTIAL_PASSWORD -> + BiometricPromptRequest.Credential.Password::class.java + Utils.CREDENTIAL_PATTERN -> + BiometricPromptRequest.Credential.Pattern::class.java + else -> throw Exception("wrong kind") + } + ) + if (p is BiometricPromptRequest.Credential.Pattern) { + assertThat(p.stealthMode).isEqualTo(isStealth) + } + + interactor.resetPrompt() + + assertThat(prompt).isNull() + + job.cancel() + } + + @Test + fun checkCredential() = + runTest(dispatcher) { + val hat = ByteArray(4) + credentialInteractor.verifyCredentialResponse = { _ -> flowOf(verified(hat)) } + + val errors = mutableListOf<CredentialStatus.Fail?>() + val job = launch { interactor.verificationError.toList(errors) } + + val checked = + interactor.checkCredential(pinRequest(), text = "1234") + as? CredentialStatus.Success.Verified + + assertThat(checked).isNotNull() + assertThat(checked!!.hat).isSameInstanceAs(hat) + assertThat(errors.map { it?.error }).containsExactly(null) + + job.cancel() + } + + @Test + fun checkCredentialWhenBad() = + runTest(dispatcher) { + val errorMessage = "bad" + val remainingAttempts = 12 + credentialInteractor.verifyCredentialResponse = { _ -> + flowOf(error(errorMessage, remainingAttempts)) + } + + val errors = mutableListOf<CredentialStatus.Fail?>() + val job = launch { interactor.verificationError.toList(errors) } + + val checked = + interactor.checkCredential(pinRequest(), text = "1234") + as? CredentialStatus.Fail.Error + + assertThat(checked).isNotNull() + assertThat(checked!!.remainingAttempts).isEqualTo(remainingAttempts) + assertThat(checked.urgentMessage).isNull() + assertThat(errors.map { it?.error }).containsExactly(null, errorMessage).inOrder() + + job.cancel() + } + + @Test + fun checkCredentialWhenBadAndUrgentMessage() = + runTest(dispatcher) { + val error = "not so bad" + val urgentMessage = "really bad" + credentialInteractor.verifyCredentialResponse = { _ -> + flowOf(error(error, 10, urgentMessage)) + } + + val errors = mutableListOf<CredentialStatus.Fail?>() + val job = launch { interactor.verificationError.toList(errors) } + + val checked = + interactor.checkCredential(pinRequest(), text = "1234") + as? CredentialStatus.Fail.Error + + assertThat(checked).isNotNull() + assertThat(checked!!.urgentMessage).isEqualTo(urgentMessage) + assertThat(errors.map { it?.error }).containsExactly(null, error).inOrder() + assertThat(errors.last() as? CredentialStatus.Fail.Error) + .isEqualTo(error(error, 10, urgentMessage)) + + job.cancel() + } + + @Test + fun checkCredentialWhenBadAndThrottled() = + runTest(dispatcher) { + val remainingAttempts = 3 + val error = ":(" + val urgentMessage = ":D" + credentialInteractor.verifyCredentialResponse = { _ -> + flow { + for (i in 1..3) { + emit(throttled("$i")) + delay(100) + } + emit(error(error, remainingAttempts, urgentMessage)) + } + } + val errors = mutableListOf<CredentialStatus.Fail?>() + val job = launch { interactor.verificationError.toList(errors) } + + val checked = + interactor.checkCredential(pinRequest(), text = "1234") + as? CredentialStatus.Fail.Error + + assertThat(checked).isNotNull() + assertThat(checked!!.remainingAttempts).isEqualTo(remainingAttempts) + assertThat(checked.urgentMessage).isEqualTo(urgentMessage) + assertThat(errors.map { it?.error }) + .containsExactly(null, "1", "2", "3", error) + .inOrder() + + job.cancel() + } +} + +private fun pinRequest(): BiometricPromptRequest.Credential.Pin = + BiometricPromptRequest.Credential.Pin( + promptInfo(), + BiometricUserInfo(USER_ID), + BiometricOperationInfo(OPERATION_ID) + ) + +private fun verified(hat: ByteArray) = CredentialStatus.Success.Verified(hat) + +private fun throttled(error: String) = CredentialStatus.Fail.Throttled(error) + +private fun error(error: String? = null, remaining: Int? = null, urgentMessage: String? = null) = + CredentialStatus.Fail.Error(error, remaining, urgentMessage) diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt new file mode 100644 index 000000000000..4c5e3c1bc6a6 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt @@ -0,0 +1,93 @@ +package com.android.systemui.biometrics.domain.model + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.biometrics.promptInfo +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +private const val USER_ID = 2 +private const val OPERATION_ID = 8L + +@SmallTest +@RunWith(JUnit4::class) +class BiometricPromptRequestTest : SysuiTestCase() { + + @Test + fun biometricRequestFromPromptInfo() { + val title = "what" + val subtitle = "a" + val description = "request" + + val request = + BiometricPromptRequest.Biometric( + promptInfo(title = title, subtitle = subtitle, description = description), + BiometricUserInfo(USER_ID), + BiometricOperationInfo(OPERATION_ID) + ) + + assertThat(request.title).isEqualTo(title) + assertThat(request.subtitle).isEqualTo(subtitle) + assertThat(request.description).isEqualTo(description) + assertThat(request.userInfo).isEqualTo(BiometricUserInfo(USER_ID)) + assertThat(request.operationInfo).isEqualTo(BiometricOperationInfo(OPERATION_ID)) + } + + @Test + fun credentialRequestFromPromptInfo() { + val title = "what" + val subtitle = "a" + val description = "request" + val stealth = true + + val toCheck = + listOf( + BiometricPromptRequest.Credential.Pin( + promptInfo( + title = title, + subtitle = subtitle, + description = description, + credentialTitle = null, + credentialSubtitle = null, + credentialDescription = null + ), + BiometricUserInfo(USER_ID), + BiometricOperationInfo(OPERATION_ID) + ), + BiometricPromptRequest.Credential.Password( + promptInfo( + credentialTitle = title, + credentialSubtitle = subtitle, + credentialDescription = description + ), + BiometricUserInfo(USER_ID), + BiometricOperationInfo(OPERATION_ID) + ), + BiometricPromptRequest.Credential.Pattern( + promptInfo( + subtitle = subtitle, + description = description, + credentialTitle = title, + credentialSubtitle = null, + credentialDescription = null + ), + BiometricUserInfo(USER_ID), + BiometricOperationInfo(OPERATION_ID), + stealth + ) + ) + + for (request in toCheck) { + assertThat(request.title).isEqualTo(title) + assertThat(request.subtitle).isEqualTo(subtitle) + assertThat(request.description).isEqualTo(description) + assertThat(request.userInfo).isEqualTo(BiometricUserInfo(USER_ID)) + assertThat(request.operationInfo).isEqualTo(BiometricOperationInfo(OPERATION_ID)) + if (request is BiometricPromptRequest.Credential.Pattern) { + assertThat(request.stealthMode).isEqualTo(stealth) + } + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModelTest.kt new file mode 100644 index 000000000000..d73cdfc4249f --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModelTest.kt @@ -0,0 +1,181 @@ +package com.android.systemui.biometrics.ui.viewmodel + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.biometrics.data.model.PromptKind +import com.android.systemui.biometrics.data.repository.FakePromptRepository +import com.android.systemui.biometrics.domain.interactor.BiometricPromptCredentialInteractor +import com.android.systemui.biometrics.domain.interactor.CredentialStatus +import com.android.systemui.biometrics.domain.interactor.FakeCredentialInteractor +import com.android.systemui.biometrics.promptInfo +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +private const val USER_ID = 9 +private const val OPERATION_ID = 10L + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(JUnit4::class) +class CredentialViewModelTest : SysuiTestCase() { + + private val dispatcher = UnconfinedTestDispatcher() + private val promptRepository = FakePromptRepository() + private val credentialInteractor = FakeCredentialInteractor() + + private lateinit var viewModel: CredentialViewModel + + @Before + fun setup() { + viewModel = + CredentialViewModel( + mContext, + BiometricPromptCredentialInteractor( + dispatcher, + promptRepository, + credentialInteractor + ) + ) + } + + @Test fun setsPinInputFlags() = setsInputFlags(PromptKind.PIN, expectFlags = true) + @Test fun setsPasswordInputFlags() = setsInputFlags(PromptKind.PASSWORD, expectFlags = false) + @Test fun setsPatternInputFlags() = setsInputFlags(PromptKind.PATTERN, expectFlags = false) + + private fun setsInputFlags(type: PromptKind, expectFlags: Boolean) = + runTestWithKind(type) { + var flags: Int? = null + val job = launch { viewModel.inputFlags.collect { flags = it } } + + if (expectFlags) { + assertThat(flags).isNotNull() + } else { + assertThat(flags).isNull() + } + job.cancel() + } + + @Test fun isStealthIgnoredByPin() = isStealthMode(PromptKind.PIN, expectStealth = false) + @Test + fun isStealthIgnoredByPassword() = isStealthMode(PromptKind.PASSWORD, expectStealth = false) + @Test fun isStealthUsedByPattern() = isStealthMode(PromptKind.PATTERN, expectStealth = true) + + private fun isStealthMode(type: PromptKind, expectStealth: Boolean) = + runTestWithKind(type, init = { credentialInteractor.stealthMode = true }) { + var stealth: Boolean? = null + val job = launch { viewModel.stealthMode.collect { stealth = it } } + + assertThat(stealth).isEqualTo(expectStealth) + + job.cancel() + } + + @Test + fun animatesContents() = runTestWithKind { + val expected = arrayOf(true, false, true) + val animate = mutableListOf<Boolean>() + val job = launch { viewModel.animateContents.toList(animate) } + + for (value in expected) { + viewModel.setAnimateContents(value) + viewModel.setAnimateContents(value) + } + assertThat(animate).containsExactly(*expected).inOrder() + + job.cancel() + } + + @Test + fun showAndClearErrors() = runTestWithKind { + var error = "" + val job = launch { viewModel.errorMessage.collect { error = it } } + assertThat(error).isEmpty() + + viewModel.showPatternTooShortError() + assertThat(error).isNotEmpty() + + viewModel.resetErrorMessage() + assertThat(error).isEmpty() + + job.cancel() + } + + @Test + fun checkCredential() = runTestWithKind { + val hat = ByteArray(2) + credentialInteractor.verifyCredentialResponse = { _ -> + flowOf(CredentialStatus.Success.Verified(hat)) + } + + val attestations = mutableListOf<ByteArray?>() + val remainingAttempts = mutableListOf<RemainingAttempts?>() + var header: HeaderViewModel? = null + val job = launch { + launch { viewModel.validatedAttestation.toList(attestations) } + launch { viewModel.remainingAttempts.toList(remainingAttempts) } + launch { viewModel.header.collect { header = it } } + } + assertThat(header).isNotNull() + + viewModel.checkCredential("p", header!!) + + val attestation = attestations.removeLastOrNull() + assertThat(attestation).isSameInstanceAs(hat) + assertThat(attestations).isEmpty() + assertThat(remainingAttempts).containsExactly(RemainingAttempts()) + + job.cancel() + } + + @Test + fun checkCredentialWhenBad() = runTestWithKind { + val remaining = 2 + val urgentError = "wow" + credentialInteractor.verifyCredentialResponse = { _ -> + flowOf(CredentialStatus.Fail.Error("error", remaining, urgentError)) + } + + val attestations = mutableListOf<ByteArray?>() + val remainingAttempts = mutableListOf<RemainingAttempts?>() + var header: HeaderViewModel? = null + val job = launch { + launch { viewModel.validatedAttestation.toList(attestations) } + launch { viewModel.remainingAttempts.toList(remainingAttempts) } + launch { viewModel.header.collect { header = it } } + } + assertThat(header).isNotNull() + + viewModel.checkCredential("1111", header!!) + + assertThat(attestations).containsExactly(null) + + val attemptInfo = remainingAttempts.removeLastOrNull() + assertThat(attemptInfo).isNotNull() + assertThat(attemptInfo!!.remaining).isEqualTo(remaining) + assertThat(attemptInfo.message).isEqualTo(urgentError) + assertThat(remainingAttempts).containsExactly(RemainingAttempts()) // initial value + + job.cancel() + } + + private fun runTestWithKind( + kind: PromptKind = PromptKind.PIN, + init: () -> Unit = {}, + block: suspend TestScope.() -> Unit, + ) = + runTest(dispatcher) { + init() + promptRepository.setPrompt(promptInfo(), USER_ID, OPERATION_ID, kind) + block() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationUtilsTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationUtilsTest.java index e099c9269d3f..ea16cb567028 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationUtilsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationUtilsTest.java @@ -20,6 +20,7 @@ import static com.android.systemui.dreams.complication.Complication.COMPLICATION import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_CAST_INFO; import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_DATE; import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_HOME_CONTROLS; +import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_MEDIA_ENTRY; import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_SMARTSPACE; import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_TIME; import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_WEATHER; @@ -63,6 +64,8 @@ public class ComplicationUtilsTest extends SysuiTestCase { .isEqualTo(COMPLICATION_TYPE_HOME_CONTROLS); assertThat(convertComplicationType(DreamBackend.COMPLICATION_TYPE_SMARTSPACE)) .isEqualTo(COMPLICATION_TYPE_SMARTSPACE); + assertThat(convertComplicationType(DreamBackend.COMPLICATION_TYPE_MEDIA_ENTRY)) + .isEqualTo(COMPLICATION_TYPE_MEDIA_ENTRY); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamMediaEntryComplicationTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamMediaEntryComplicationTest.java index 50f27ea27ae9..0295030da510 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamMediaEntryComplicationTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamMediaEntryComplicationTest.java @@ -16,8 +16,11 @@ package com.android.systemui.dreams.complication; +import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_MEDIA_ENTRY; import static com.android.systemui.flags.Flags.DREAM_MEDIA_TAP_TO_OPEN; +import static com.google.common.truth.Truth.assertThat; + import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -32,6 +35,7 @@ import androidx.test.filters.SmallTest; import com.android.systemui.ActivityIntentHelper; import com.android.systemui.SysuiTestCase; import com.android.systemui.dreams.DreamOverlayStateController; +import com.android.systemui.dreams.complication.dagger.DreamMediaEntryComplicationComponent; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.media.controls.ui.MediaCarouselController; import com.android.systemui.media.dream.MediaDreamComplication; @@ -51,6 +55,9 @@ import org.mockito.MockitoAnnotations; @TestableLooper.RunWithLooper public class DreamMediaEntryComplicationTest extends SysuiTestCase { @Mock + private DreamMediaEntryComplicationComponent.Factory mComponentFactory; + + @Mock private View mView; @Mock @@ -89,6 +96,14 @@ public class DreamMediaEntryComplicationTest extends SysuiTestCase { when(mFeatureFlags.isEnabled(DREAM_MEDIA_TAP_TO_OPEN)).thenReturn(false); } + @Test + public void testGetRequiredTypeAvailability() { + final DreamMediaEntryComplication complication = + new DreamMediaEntryComplication(mComponentFactory); + assertThat(complication.getRequiredTypeAvailability()).isEqualTo( + COMPLICATION_TYPE_MEDIA_ENTRY); + } + /** * Ensures clicking media entry chip adds/removes media complication. */ diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsDebugTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsDebugTest.kt index 20a82c63cfdd..4b3b70e3ae77 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsDebugTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsDebugTest.kt @@ -88,6 +88,7 @@ class FeatureFlagsDebugTest : SysuiTestCase() { flagMap, restarter ) + mFeatureFlagsDebug.init() verify(flagManager).onSettingsChangedAction = any() broadcastReceiver = withArgCaptor { verify(mockContext).registerReceiver(capture(), any(), nullable(), nullable(), @@ -255,11 +256,11 @@ class FeatureFlagsDebugTest : SysuiTestCase() { broadcastReceiver.onReceive(mockContext, Intent()) broadcastReceiver.onReceive(mockContext, Intent("invalid action")) broadcastReceiver.onReceive(mockContext, Intent(FlagManager.ACTION_SET_FLAG)) - setByBroadcast(0, false) // unknown id does nothing - setByBroadcast(1, "string") // wrong type does nothing - setByBroadcast(2, 123) // wrong type does nothing - setByBroadcast(3, false) // wrong type does nothing - setByBroadcast(4, 123) // wrong type does nothing + setByBroadcast(0, false) // unknown id does nothing + setByBroadcast(1, "string") // wrong type does nothing + setByBroadcast(2, 123) // wrong type does nothing + setByBroadcast(3, false) // wrong type does nothing + setByBroadcast(4, 123) // wrong type does nothing verifyNoMoreInteractions(flagManager, secureSettings) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsReleaseTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsReleaseTest.kt index 575c14262b74..b2dd60c9566d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsReleaseTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsReleaseTest.kt @@ -38,8 +38,9 @@ class FeatureFlagsReleaseTest : SysuiTestCase() { @Mock private lateinit var mResources: Resources @Mock private lateinit var mSystemProperties: SystemPropertiesHelper + @Mock private lateinit var restarter: Restarter + private val flagMap = mutableMapOf<Int, Flag<*>>() private val serverFlagReader = ServerFlagReaderFake() - private val deviceConfig = DeviceConfigProxyFake() @Before @@ -49,7 +50,9 @@ class FeatureFlagsReleaseTest : SysuiTestCase() { mResources, mSystemProperties, deviceConfig, - serverFlagReader) + serverFlagReader, + flagMap, + restarter) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/ServerFlagReaderImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/ServerFlagReaderImplTest.kt new file mode 100644 index 000000000000..6f5f460d41c4 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/flags/ServerFlagReaderImplTest.kt @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.flags + +import android.test.suitebuilder.annotation.SmallTest +import android.testing.AndroidTestingRunner +import com.android.systemui.SysuiTestCase +import com.android.systemui.util.DeviceConfigProxyFake +import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.time.FakeSystemClock +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class ServerFlagReaderImplTest : SysuiTestCase() { + + private val NAMESPACE = "test" + + @Mock private lateinit var changeListener: ServerFlagReader.ChangeListener + + private lateinit var serverFlagReader: ServerFlagReaderImpl + private val deviceConfig = DeviceConfigProxyFake() + private val executor = FakeExecutor(FakeSystemClock()) + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + + serverFlagReader = ServerFlagReaderImpl(NAMESPACE, deviceConfig, executor) + } + + @Test + fun testChange_alertsListener() { + val flag = ReleasedFlag(1) + serverFlagReader.listenForChanges(listOf(flag), changeListener) + + deviceConfig.setProperty(NAMESPACE, "flag_override_1", "1", false) + executor.runAllReady() + + verify(changeListener).onChange() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/LockIconViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/LockIconViewControllerTest.java deleted file mode 100644 index 27a5190367a8..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/LockIconViewControllerTest.java +++ /dev/null @@ -1,478 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.keyguard; - -import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT; - -import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; -import static com.android.keyguard.LockIconView.ICON_LOCK; -import static com.android.keyguard.LockIconView.ICON_UNLOCK; - -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.anyBoolean; -import static org.mockito.Mockito.anyInt; -import static org.mockito.Mockito.eq; -import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.content.Context; -import android.content.res.Resources; -import android.graphics.Point; -import android.graphics.Rect; -import android.graphics.drawable.AnimatedStateListDrawable; -import android.hardware.biometrics.BiometricSourceType; -import android.testing.AndroidTestingRunner; -import android.testing.TestableLooper; -import android.util.Pair; -import android.view.View; -import android.view.WindowManager; -import android.view.accessibility.AccessibilityManager; - -import androidx.test.filters.SmallTest; - -import com.android.keyguard.KeyguardUpdateMonitor; -import com.android.keyguard.KeyguardUpdateMonitorCallback; -import com.android.keyguard.KeyguardViewController; -import com.android.keyguard.LockIconView; -import com.android.keyguard.LockIconViewController; -import com.android.systemui.R; -import com.android.systemui.SysuiTestCase; -import com.android.systemui.biometrics.AuthController; -import com.android.systemui.biometrics.AuthRippleController; -import com.android.systemui.doze.util.BurnInHelperKt; -import com.android.systemui.dump.DumpManager; -import com.android.systemui.plugins.FalsingManager; -import com.android.systemui.plugins.statusbar.StatusBarStateController; -import com.android.systemui.statusbar.StatusBarState; -import com.android.systemui.statusbar.VibratorHelper; -import com.android.systemui.statusbar.policy.ConfigurationController; -import com.android.systemui.statusbar.policy.KeyguardStateController; -import com.android.systemui.util.concurrency.FakeExecutor; -import com.android.systemui.util.time.FakeSystemClock; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Answers; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.mockito.MockitoSession; -import org.mockito.quality.Strictness; - -@SmallTest -@RunWith(AndroidTestingRunner.class) -@TestableLooper.RunWithLooper -public class LockIconViewControllerTest extends SysuiTestCase { - private static final String UNLOCKED_LABEL = "unlocked"; - private static final int PADDING = 10; - - private MockitoSession mStaticMockSession; - - private @Mock LockIconView mLockIconView; - private @Mock AnimatedStateListDrawable mIconDrawable; - private @Mock Context mContext; - private @Mock Resources mResources; - private @Mock(answer = Answers.RETURNS_DEEP_STUBS) WindowManager mWindowManager; - private @Mock StatusBarStateController mStatusBarStateController; - private @Mock KeyguardUpdateMonitor mKeyguardUpdateMonitor; - private @Mock KeyguardViewController mKeyguardViewController; - private @Mock KeyguardStateController mKeyguardStateController; - private @Mock FalsingManager mFalsingManager; - private @Mock AuthController mAuthController; - private @Mock DumpManager mDumpManager; - private @Mock AccessibilityManager mAccessibilityManager; - private @Mock ConfigurationController mConfigurationController; - private @Mock VibratorHelper mVibrator; - private @Mock AuthRippleController mAuthRippleController; - private FakeExecutor mDelayableExecutor = new FakeExecutor(new FakeSystemClock()); - - private LockIconViewController mLockIconViewController; - - // Capture listeners so that they can be used to send events - @Captor private ArgumentCaptor<View.OnAttachStateChangeListener> mAttachCaptor = - ArgumentCaptor.forClass(View.OnAttachStateChangeListener.class); - private View.OnAttachStateChangeListener mAttachListener; - - @Captor private ArgumentCaptor<KeyguardStateController.Callback> mKeyguardStateCaptor = - ArgumentCaptor.forClass(KeyguardStateController.Callback.class); - private KeyguardStateController.Callback mKeyguardStateCallback; - - @Captor private ArgumentCaptor<StatusBarStateController.StateListener> mStatusBarStateCaptor = - ArgumentCaptor.forClass(StatusBarStateController.StateListener.class); - private StatusBarStateController.StateListener mStatusBarStateListener; - - @Captor private ArgumentCaptor<AuthController.Callback> mAuthControllerCallbackCaptor; - private AuthController.Callback mAuthControllerCallback; - - @Captor private ArgumentCaptor<KeyguardUpdateMonitorCallback> - mKeyguardUpdateMonitorCallbackCaptor = - ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback.class); - private KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback; - - @Captor private ArgumentCaptor<Point> mPointCaptor; - - @Before - public void setUp() throws Exception { - mStaticMockSession = mockitoSession() - .mockStatic(BurnInHelperKt.class) - .strictness(Strictness.LENIENT) - .startMocking(); - MockitoAnnotations.initMocks(this); - - setupLockIconViewMocks(); - when(mContext.getResources()).thenReturn(mResources); - when(mContext.getSystemService(WindowManager.class)).thenReturn(mWindowManager); - Rect windowBounds = new Rect(0, 0, 800, 1200); - when(mWindowManager.getCurrentWindowMetrics().getBounds()).thenReturn(windowBounds); - when(mResources.getString(R.string.accessibility_unlock_button)).thenReturn(UNLOCKED_LABEL); - when(mResources.getDrawable(anyInt(), any())).thenReturn(mIconDrawable); - when(mResources.getDimensionPixelSize(R.dimen.lock_icon_padding)).thenReturn(PADDING); - when(mAuthController.getScaleFactor()).thenReturn(1f); - - when(mKeyguardStateController.isShowing()).thenReturn(true); - when(mKeyguardStateController.isKeyguardGoingAway()).thenReturn(false); - when(mStatusBarStateController.isDozing()).thenReturn(false); - when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD); - - mLockIconViewController = new LockIconViewController( - mLockIconView, - mStatusBarStateController, - mKeyguardUpdateMonitor, - mKeyguardViewController, - mKeyguardStateController, - mFalsingManager, - mAuthController, - mDumpManager, - mAccessibilityManager, - mConfigurationController, - mDelayableExecutor, - mVibrator, - mAuthRippleController, - mResources - ); - } - - @After - public void tearDown() { - mStaticMockSession.finishMocking(); - } - - @Test - public void testUpdateFingerprintLocationOnInit() { - // GIVEN fp sensor location is available pre-attached - Pair<Float, Point> udfps = setupUdfps(); // first = radius, second = udfps location - - // WHEN lock icon view controller is initialized and attached - mLockIconViewController.init(); - captureAttachListener(); - mAttachListener.onViewAttachedToWindow(mLockIconView); - - // THEN lock icon view location is updated to the udfps location with UDFPS radius - verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first), - eq(PADDING)); - } - - @Test - public void testUpdatePaddingBasedOnResolutionScale() { - // GIVEN fp sensor location is available pre-attached & scaled resolution factor is 5 - Pair<Float, Point> udfps = setupUdfps(); // first = radius, second = udfps location - when(mAuthController.getScaleFactor()).thenReturn(5f); - - // WHEN lock icon view controller is initialized and attached - mLockIconViewController.init(); - captureAttachListener(); - mAttachListener.onViewAttachedToWindow(mLockIconView); - - // THEN lock icon view location is updated with the scaled radius - verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first), - eq(PADDING * 5)); - } - - @Test - public void testUpdateLockIconLocationOnAuthenticatorsRegistered() { - // GIVEN fp sensor location is not available pre-init - when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(false); - when(mAuthController.getFingerprintSensorLocation()).thenReturn(null); - mLockIconViewController.init(); - captureAttachListener(); - mAttachListener.onViewAttachedToWindow(mLockIconView); - resetLockIconView(); // reset any method call counts for when we verify method calls later - - // GIVEN fp sensor location is available post-attached - captureAuthControllerCallback(); - Pair<Float, Point> udfps = setupUdfps(); - - // WHEN all authenticators are registered - mAuthControllerCallback.onAllAuthenticatorsRegistered(TYPE_FINGERPRINT); - mDelayableExecutor.runAllReady(); - - // THEN lock icon view location is updated with the same coordinates as auth controller vals - verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first), - eq(PADDING)); - } - - @Test - public void testUpdateLockIconLocationOnUdfpsLocationChanged() { - // GIVEN fp sensor location is not available pre-init - when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(false); - when(mAuthController.getFingerprintSensorLocation()).thenReturn(null); - mLockIconViewController.init(); - captureAttachListener(); - mAttachListener.onViewAttachedToWindow(mLockIconView); - resetLockIconView(); // reset any method call counts for when we verify method calls later - - // GIVEN fp sensor location is available post-attached - captureAuthControllerCallback(); - Pair<Float, Point> udfps = setupUdfps(); - - // WHEN udfps location changes - mAuthControllerCallback.onUdfpsLocationChanged(); - mDelayableExecutor.runAllReady(); - - // THEN lock icon view location is updated with the same coordinates as auth controller vals - verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first), - eq(PADDING)); - } - - @Test - public void testLockIconViewBackgroundEnabledWhenUdfpsIsSupported() { - // GIVEN Udpfs sensor location is available - setupUdfps(); - - mLockIconViewController.init(); - captureAttachListener(); - - // WHEN the view is attached - mAttachListener.onViewAttachedToWindow(mLockIconView); - - // THEN the lock icon view background should be enabled - verify(mLockIconView).setUseBackground(true); - } - - @Test - public void testLockIconViewBackgroundDisabledWhenUdfpsIsNotSupported() { - // GIVEN Udfps sensor location is not supported - when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(false); - - mLockIconViewController.init(); - captureAttachListener(); - - // WHEN the view is attached - mAttachListener.onViewAttachedToWindow(mLockIconView); - - // THEN the lock icon view background should be disabled - verify(mLockIconView).setUseBackground(false); - } - - @Test - public void testUnlockIconShows_biometricUnlockedTrue() { - // GIVEN UDFPS sensor location is available - setupUdfps(); - - // GIVEN lock icon controller is initialized and view is attached - mLockIconViewController.init(); - captureAttachListener(); - mAttachListener.onViewAttachedToWindow(mLockIconView); - captureKeyguardUpdateMonitorCallback(); - - // GIVEN user has unlocked with a biometric auth (ie: face auth) - when(mKeyguardUpdateMonitor.getUserUnlockedWithBiometric(anyInt())).thenReturn(true); - reset(mLockIconView); - - // WHEN face auth's biometric running state changes - mKeyguardUpdateMonitorCallback.onBiometricRunningStateChanged(false, - BiometricSourceType.FACE); - - // THEN the unlock icon is shown - verify(mLockIconView).setContentDescription(UNLOCKED_LABEL); - } - - @Test - public void testLockIconStartState() { - // GIVEN lock icon state - setupShowLockIcon(); - - // WHEN lock icon controller is initialized - mLockIconViewController.init(); - captureAttachListener(); - mAttachListener.onViewAttachedToWindow(mLockIconView); - - // THEN the lock icon should show - verify(mLockIconView).updateIcon(ICON_LOCK, false); - } - - @Test - public void testLockIcon_updateToUnlock() { - // GIVEN starting state for the lock icon - setupShowLockIcon(); - - // GIVEN lock icon controller is initialized and view is attached - mLockIconViewController.init(); - captureAttachListener(); - mAttachListener.onViewAttachedToWindow(mLockIconView); - captureKeyguardStateCallback(); - reset(mLockIconView); - - // WHEN the unlocked state changes to canDismissLockScreen=true - when(mKeyguardStateController.canDismissLockScreen()).thenReturn(true); - mKeyguardStateCallback.onUnlockedChanged(); - - // THEN the unlock should show - verify(mLockIconView).updateIcon(ICON_UNLOCK, false); - } - - @Test - public void testLockIcon_clearsIconOnAod_whenUdfpsNotEnrolled() { - // GIVEN udfps not enrolled - setupUdfps(); - when(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(false); - - // GIVEN starting state for the lock icon - setupShowLockIcon(); - - // GIVEN lock icon controller is initialized and view is attached - mLockIconViewController.init(); - captureAttachListener(); - mAttachListener.onViewAttachedToWindow(mLockIconView); - captureStatusBarStateListener(); - reset(mLockIconView); - - // WHEN the dozing state changes - mStatusBarStateListener.onDozingChanged(true /* isDozing */); - - // THEN the icon is cleared - verify(mLockIconView).clearIcon(); - } - - @Test - public void testLockIcon_updateToAodLock_whenUdfpsEnrolled() { - // GIVEN udfps enrolled - setupUdfps(); - when(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(true); - - // GIVEN starting state for the lock icon - setupShowLockIcon(); - - // GIVEN lock icon controller is initialized and view is attached - mLockIconViewController.init(); - captureAttachListener(); - mAttachListener.onViewAttachedToWindow(mLockIconView); - captureStatusBarStateListener(); - reset(mLockIconView); - - // WHEN the dozing state changes - mStatusBarStateListener.onDozingChanged(true /* isDozing */); - - // THEN the AOD lock icon should show - verify(mLockIconView).updateIcon(ICON_LOCK, true); - } - - @Test - public void testBurnInOffsetsUpdated_onDozeAmountChanged() { - // GIVEN udfps enrolled - setupUdfps(); - when(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(true); - - // GIVEN burn-in offset = 5 - int burnInOffset = 5; - when(BurnInHelperKt.getBurnInOffset(anyInt(), anyBoolean())).thenReturn(burnInOffset); - - // GIVEN starting state for the lock icon (keyguard) - setupShowLockIcon(); - mLockIconViewController.init(); - captureAttachListener(); - mAttachListener.onViewAttachedToWindow(mLockIconView); - captureStatusBarStateListener(); - reset(mLockIconView); - - // WHEN dozing updates - mStatusBarStateListener.onDozingChanged(true /* isDozing */); - mStatusBarStateListener.onDozeAmountChanged(1f, 1f); - - // THEN the view's translation is updated to use the AoD burn-in offsets - verify(mLockIconView).setTranslationY(burnInOffset); - verify(mLockIconView).setTranslationX(burnInOffset); - reset(mLockIconView); - - // WHEN the device is no longer dozing - mStatusBarStateListener.onDozingChanged(false /* isDozing */); - mStatusBarStateListener.onDozeAmountChanged(0f, 0f); - - // THEN the view is updated to NO translation (no burn-in offsets anymore) - verify(mLockIconView).setTranslationY(0); - verify(mLockIconView).setTranslationX(0); - - } - private Pair<Float, Point> setupUdfps() { - when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(true); - final Point udfpsLocation = new Point(50, 75); - final float radius = 33f; - when(mAuthController.getUdfpsLocation()).thenReturn(udfpsLocation); - when(mAuthController.getUdfpsRadius()).thenReturn(radius); - - return new Pair(radius, udfpsLocation); - } - - private void setupShowLockIcon() { - when(mKeyguardStateController.isShowing()).thenReturn(true); - when(mKeyguardStateController.isKeyguardGoingAway()).thenReturn(false); - when(mStatusBarStateController.isDozing()).thenReturn(false); - when(mStatusBarStateController.getDozeAmount()).thenReturn(0f); - when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD); - when(mKeyguardStateController.canDismissLockScreen()).thenReturn(false); - } - - private void captureAuthControllerCallback() { - verify(mAuthController).addCallback(mAuthControllerCallbackCaptor.capture()); - mAuthControllerCallback = mAuthControllerCallbackCaptor.getValue(); - } - - private void captureAttachListener() { - verify(mLockIconView).addOnAttachStateChangeListener(mAttachCaptor.capture()); - mAttachListener = mAttachCaptor.getValue(); - } - - private void captureKeyguardStateCallback() { - verify(mKeyguardStateController).addCallback(mKeyguardStateCaptor.capture()); - mKeyguardStateCallback = mKeyguardStateCaptor.getValue(); - } - - private void captureStatusBarStateListener() { - verify(mStatusBarStateController).addCallback(mStatusBarStateCaptor.capture()); - mStatusBarStateListener = mStatusBarStateCaptor.getValue(); - } - - private void captureKeyguardUpdateMonitorCallback() { - verify(mKeyguardUpdateMonitor).registerCallback( - mKeyguardUpdateMonitorCallbackCaptor.capture()); - mKeyguardUpdateMonitorCallback = mKeyguardUpdateMonitorCallbackCaptor.getValue(); - } - - private void setupLockIconViewMocks() { - when(mLockIconView.getResources()).thenReturn(mResources); - when(mLockIconView.getContext()).thenReturn(mContext); - } - - private void resetLockIconView() { - reset(mLockIconView); - setupLockIconViewMocks(); - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/FakeKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/FakeKeyguardQuickAffordanceConfig.kt index e99c139e9e7e..f18acbad14f2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/FakeKeyguardQuickAffordanceConfig.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/FakeKeyguardQuickAffordanceConfig.kt @@ -15,10 +15,10 @@ * */ -package com.android.systemui.keyguard.domain.quickaffordance +package com.android.systemui.keyguard.data.quickaffordance import com.android.systemui.animation.Expandable -import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceConfig.OnClickedResult +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig.OnTriggeredResult import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.yield @@ -29,24 +29,27 @@ import kotlinx.coroutines.yield * This class is abstract to force tests to provide extensions of it as the system that references * these configs uses each implementation's class type to refer to them. */ -abstract class FakeKeyguardQuickAffordanceConfig : KeyguardQuickAffordanceConfig { +abstract class FakeKeyguardQuickAffordanceConfig( + override val key: String, +) : KeyguardQuickAffordanceConfig { - var onClickedResult: OnClickedResult = OnClickedResult.Handled + var onTriggeredResult: OnTriggeredResult = OnTriggeredResult.Handled - private val _state = - MutableStateFlow<KeyguardQuickAffordanceConfig.State>( - KeyguardQuickAffordanceConfig.State.Hidden + private val _lockScreenState = + MutableStateFlow<KeyguardQuickAffordanceConfig.LockScreenState>( + KeyguardQuickAffordanceConfig.LockScreenState.Hidden ) - override val state: Flow<KeyguardQuickAffordanceConfig.State> = _state + override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState> = + _lockScreenState - override fun onQuickAffordanceClicked( + override fun onTriggered( expandable: Expandable?, - ): OnClickedResult { - return onClickedResult + ): OnTriggeredResult { + return onTriggeredResult } - suspend fun setState(state: KeyguardQuickAffordanceConfig.State) { - _state.value = state + suspend fun setState(lockScreenState: KeyguardQuickAffordanceConfig.LockScreenState) { + _lockScreenState.value = lockScreenState // Yield to allow the test's collection coroutine to "catch up" and collect this value // before the test continues to the next line. // TODO(b/239834928): once coroutines.test is updated, switch to the approach described in diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt index 9a91ea91f3a2..c94cec6e313a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt @@ -1,21 +1,21 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2022 The Android Open Source Project * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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 + * 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. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. * */ -package com.android.systemui.keyguard.domain.quickaffordance +package com.android.systemui.keyguard.data.quickaffordance import androidx.test.filters.SmallTest import com.android.systemui.R @@ -122,8 +122,8 @@ class HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest : SysuiTes emptyList() } ) - val values = mutableListOf<KeyguardQuickAffordanceConfig.State>() - val job = underTest.state.onEach(values::add).launchIn(this) + val values = mutableListOf<KeyguardQuickAffordanceConfig.LockScreenState>() + val job = underTest.lockScreenState.onEach(values::add).launchIn(this) if (canShowWhileLocked) { verify(controlsListingController).addCallback(callbackCaptor.capture()) @@ -139,9 +139,9 @@ class HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest : SysuiTes assertThat(values.last()) .isInstanceOf( if (isVisibleExpected) { - KeyguardQuickAffordanceConfig.State.Visible::class.java + KeyguardQuickAffordanceConfig.LockScreenState.Visible::class.java } else { - KeyguardQuickAffordanceConfig.State.Hidden::class.java + KeyguardQuickAffordanceConfig.LockScreenState.Hidden::class.java } ) job.cancel() diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigTest.kt index a809f0547ee6..659c1e573ca3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigTest.kt @@ -1,21 +1,21 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2022 The Android Open Source Project * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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 + * 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. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. * */ -package com.android.systemui.keyguard.domain.quickaffordance +package com.android.systemui.keyguard.data.quickaffordance import androidx.test.filters.SmallTest import com.android.systemui.R @@ -23,7 +23,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.animation.Expandable import com.android.systemui.controls.controller.ControlsController import com.android.systemui.controls.dagger.ControlsComponent -import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceConfig.OnClickedResult +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig.OnTriggeredResult import com.android.systemui.util.mockito.mock import com.google.common.truth.Truth.assertThat import java.util.Optional @@ -72,11 +72,11 @@ class HomeControlsKeyguardQuickAffordanceConfigTest : SysuiTestCase() { whenever(component.getVisibility()).thenReturn(ControlsComponent.Visibility.AVAILABLE) whenever(controlsController.getFavorites()).thenReturn(listOf(mock())) - val values = mutableListOf<KeyguardQuickAffordanceConfig.State>() - val job = underTest.state.onEach(values::add).launchIn(this) + val values = mutableListOf<KeyguardQuickAffordanceConfig.LockScreenState>() + val job = underTest.lockScreenState.onEach(values::add).launchIn(this) assertThat(values.last()) - .isInstanceOf(KeyguardQuickAffordanceConfig.State.Hidden::class.java) + .isInstanceOf(KeyguardQuickAffordanceConfig.LockScreenState.Hidden::class.java) job.cancel() } @@ -91,31 +91,32 @@ class HomeControlsKeyguardQuickAffordanceConfigTest : SysuiTestCase() { whenever(component.getVisibility()).thenReturn(ControlsComponent.Visibility.AVAILABLE) whenever(controlsController.getFavorites()).thenReturn(listOf(mock())) - val values = mutableListOf<KeyguardQuickAffordanceConfig.State>() - val job = underTest.state.onEach(values::add).launchIn(this) + val values = mutableListOf<KeyguardQuickAffordanceConfig.LockScreenState>() + val job = underTest.lockScreenState.onEach(values::add).launchIn(this) assertThat(values.last()) - .isInstanceOf(KeyguardQuickAffordanceConfig.State.Hidden::class.java) + .isInstanceOf(KeyguardQuickAffordanceConfig.LockScreenState.Hidden::class.java) job.cancel() } @Test - fun `onQuickAffordanceClicked - canShowWhileLockedSetting is true`() = runBlockingTest { + fun `onQuickAffordanceTriggered - canShowWhileLockedSetting is true`() = runBlockingTest { whenever(component.canShowWhileLockedSetting).thenReturn(MutableStateFlow(true)) - val onClickedResult = underTest.onQuickAffordanceClicked(expandable) + val onClickedResult = underTest.onTriggered(expandable) - assertThat(onClickedResult).isInstanceOf(OnClickedResult.StartActivity::class.java) - assertThat((onClickedResult as OnClickedResult.StartActivity).canShowWhileLocked).isTrue() + assertThat(onClickedResult).isInstanceOf(OnTriggeredResult.StartActivity::class.java) + assertThat((onClickedResult as OnTriggeredResult.StartActivity).canShowWhileLocked).isTrue() } @Test - fun `onQuickAffordanceClicked - canShowWhileLockedSetting is false`() = runBlockingTest { + fun `onQuickAffordanceTriggered - canShowWhileLockedSetting is false`() = runBlockingTest { whenever(component.canShowWhileLockedSetting).thenReturn(MutableStateFlow(false)) - val onClickedResult = underTest.onQuickAffordanceClicked(expandable) + val onClickedResult = underTest.onTriggered(expandable) - assertThat(onClickedResult).isInstanceOf(OnClickedResult.StartActivity::class.java) - assertThat((onClickedResult as OnClickedResult.StartActivity).canShowWhileLocked).isFalse() + assertThat(onClickedResult).isInstanceOf(OnTriggeredResult.StartActivity::class.java) + assertThat((onClickedResult as OnTriggeredResult.StartActivity).canShowWhileLocked) + .isFalse() } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt index 329c4db0a75c..61a3f9f07600 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt @@ -1,26 +1,26 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2022 The Android Open Source Project * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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 + * 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. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. * */ -package com.android.systemui.keyguard.domain.quickaffordance +package com.android.systemui.keyguard.data.quickaffordance import android.content.Intent import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceConfig.OnClickedResult +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig.OnTriggeredResult import com.android.systemui.qrcodescanner.controller.QRCodeScannerController import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.mock @@ -56,9 +56,9 @@ class QrCodeScannerKeyguardQuickAffordanceConfigTest : SysuiTestCase() { @Test fun `affordance - sets up registration and delivers initial model`() = runBlockingTest { whenever(controller.isEnabledForLockScreenButton).thenReturn(true) - var latest: KeyguardQuickAffordanceConfig.State? = null + var latest: KeyguardQuickAffordanceConfig.LockScreenState? = null - val job = underTest.state.onEach { latest = it }.launchIn(this) + val job = underTest.lockScreenState.onEach { latest = it }.launchIn(this) val callbackCaptor = argumentCaptor<QRCodeScannerController.Callback>() verify(controller).addCallback(callbackCaptor.capture()) @@ -77,8 +77,8 @@ class QrCodeScannerKeyguardQuickAffordanceConfigTest : SysuiTestCase() { fun `affordance - scanner activity changed - delivers model with updated intent`() = runBlockingTest { whenever(controller.isEnabledForLockScreenButton).thenReturn(true) - var latest: KeyguardQuickAffordanceConfig.State? = null - val job = underTest.state.onEach { latest = it }.launchIn(this) + var latest: KeyguardQuickAffordanceConfig.LockScreenState? = null + val job = underTest.lockScreenState.onEach { latest = it }.launchIn(this) val callbackCaptor = argumentCaptor<QRCodeScannerController.Callback>() verify(controller).addCallback(callbackCaptor.capture()) @@ -93,8 +93,8 @@ class QrCodeScannerKeyguardQuickAffordanceConfigTest : SysuiTestCase() { @Test fun `affordance - scanner preference changed - delivers visible model`() = runBlockingTest { - var latest: KeyguardQuickAffordanceConfig.State? = null - val job = underTest.state.onEach { latest = it }.launchIn(this) + var latest: KeyguardQuickAffordanceConfig.LockScreenState? = null + val job = underTest.lockScreenState.onEach { latest = it }.launchIn(this) val callbackCaptor = argumentCaptor<QRCodeScannerController.Callback>() verify(controller).addCallback(callbackCaptor.capture()) @@ -109,34 +109,35 @@ class QrCodeScannerKeyguardQuickAffordanceConfigTest : SysuiTestCase() { @Test fun `affordance - scanner preference changed - delivers none`() = runBlockingTest { - var latest: KeyguardQuickAffordanceConfig.State? = null - val job = underTest.state.onEach { latest = it }.launchIn(this) + var latest: KeyguardQuickAffordanceConfig.LockScreenState? = null + val job = underTest.lockScreenState.onEach { latest = it }.launchIn(this) val callbackCaptor = argumentCaptor<QRCodeScannerController.Callback>() verify(controller).addCallback(callbackCaptor.capture()) whenever(controller.isEnabledForLockScreenButton).thenReturn(false) callbackCaptor.value.onQRCodeScannerPreferenceChanged() - assertThat(latest).isEqualTo(KeyguardQuickAffordanceConfig.State.Hidden) + assertThat(latest).isEqualTo(KeyguardQuickAffordanceConfig.LockScreenState.Hidden) job.cancel() verify(controller).removeCallback(callbackCaptor.value) } @Test - fun onQuickAffordanceClicked() { - assertThat(underTest.onQuickAffordanceClicked(mock())) + fun onQuickAffordanceTriggered() { + assertThat(underTest.onTriggered(mock())) .isEqualTo( - OnClickedResult.StartActivity( + OnTriggeredResult.StartActivity( intent = INTENT_1, canShowWhileLocked = true, ) ) } - private fun assertVisibleState(latest: KeyguardQuickAffordanceConfig.State?) { - assertThat(latest).isInstanceOf(KeyguardQuickAffordanceConfig.State.Visible::class.java) - val visibleState = latest as KeyguardQuickAffordanceConfig.State.Visible + private fun assertVisibleState(latest: KeyguardQuickAffordanceConfig.LockScreenState?) { + assertThat(latest) + .isInstanceOf(KeyguardQuickAffordanceConfig.LockScreenState.Visible::class.java) + val visibleState = latest as KeyguardQuickAffordanceConfig.LockScreenState.Visible assertThat(visibleState.icon).isNotNull() assertThat(visibleState.icon.contentDescription).isNotNull() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt index 98dc4c4f6f76..c05beef6d624 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt @@ -1,21 +1,21 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2022 The Android Open Source Project * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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 + * 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. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. * */ -package com.android.systemui.keyguard.domain.quickaffordance +package com.android.systemui.keyguard.data.quickaffordance import android.graphics.drawable.Drawable import android.service.quickaccesswallet.GetWalletCardsResponse @@ -67,11 +67,11 @@ class QuickAccessWalletKeyguardQuickAffordanceConfigTest : SysuiTestCase() { @Test fun `affordance - keyguard showing - has wallet card - visible model`() = runBlockingTest { setUpState() - var latest: KeyguardQuickAffordanceConfig.State? = null + var latest: KeyguardQuickAffordanceConfig.LockScreenState? = null - val job = underTest.state.onEach { latest = it }.launchIn(this) + val job = underTest.lockScreenState.onEach { latest = it }.launchIn(this) - val visibleModel = latest as KeyguardQuickAffordanceConfig.State.Visible + val visibleModel = latest as KeyguardQuickAffordanceConfig.LockScreenState.Visible assertThat(visibleModel.icon) .isEqualTo( Icon.Loaded( @@ -88,11 +88,11 @@ class QuickAccessWalletKeyguardQuickAffordanceConfigTest : SysuiTestCase() { @Test fun `affordance - wallet not enabled - model is none`() = runBlockingTest { setUpState(isWalletEnabled = false) - var latest: KeyguardQuickAffordanceConfig.State? = null + var latest: KeyguardQuickAffordanceConfig.LockScreenState? = null - val job = underTest.state.onEach { latest = it }.launchIn(this) + val job = underTest.lockScreenState.onEach { latest = it }.launchIn(this) - assertThat(latest).isEqualTo(KeyguardQuickAffordanceConfig.State.Hidden) + assertThat(latest).isEqualTo(KeyguardQuickAffordanceConfig.LockScreenState.Hidden) job.cancel() } @@ -100,11 +100,11 @@ class QuickAccessWalletKeyguardQuickAffordanceConfigTest : SysuiTestCase() { @Test fun `affordance - query not successful - model is none`() = runBlockingTest { setUpState(isWalletQuerySuccessful = false) - var latest: KeyguardQuickAffordanceConfig.State? = null + var latest: KeyguardQuickAffordanceConfig.LockScreenState? = null - val job = underTest.state.onEach { latest = it }.launchIn(this) + val job = underTest.lockScreenState.onEach { latest = it }.launchIn(this) - assertThat(latest).isEqualTo(KeyguardQuickAffordanceConfig.State.Hidden) + assertThat(latest).isEqualTo(KeyguardQuickAffordanceConfig.LockScreenState.Hidden) job.cancel() } @@ -112,11 +112,11 @@ class QuickAccessWalletKeyguardQuickAffordanceConfigTest : SysuiTestCase() { @Test fun `affordance - missing icon - model is none`() = runBlockingTest { setUpState(hasWalletIcon = false) - var latest: KeyguardQuickAffordanceConfig.State? = null + var latest: KeyguardQuickAffordanceConfig.LockScreenState? = null - val job = underTest.state.onEach { latest = it }.launchIn(this) + val job = underTest.lockScreenState.onEach { latest = it }.launchIn(this) - assertThat(latest).isEqualTo(KeyguardQuickAffordanceConfig.State.Hidden) + assertThat(latest).isEqualTo(KeyguardQuickAffordanceConfig.LockScreenState.Hidden) job.cancel() } @@ -124,24 +124,24 @@ class QuickAccessWalletKeyguardQuickAffordanceConfigTest : SysuiTestCase() { @Test fun `affordance - no selected card - model is none`() = runBlockingTest { setUpState(hasWalletIcon = false) - var latest: KeyguardQuickAffordanceConfig.State? = null + var latest: KeyguardQuickAffordanceConfig.LockScreenState? = null - val job = underTest.state.onEach { latest = it }.launchIn(this) + val job = underTest.lockScreenState.onEach { latest = it }.launchIn(this) - assertThat(latest).isEqualTo(KeyguardQuickAffordanceConfig.State.Hidden) + assertThat(latest).isEqualTo(KeyguardQuickAffordanceConfig.LockScreenState.Hidden) job.cancel() } @Test - fun onQuickAffordanceClicked() { + fun onQuickAffordanceTriggered() { val animationController: ActivityLaunchAnimator.Controller = mock() val expandable: Expandable = mock { whenever(this.activityLaunchController()).thenReturn(animationController) } - assertThat(underTest.onQuickAffordanceClicked(expandable)) - .isEqualTo(KeyguardQuickAffordanceConfig.OnClickedResult.Handled) + assertThat(underTest.onTriggered(expandable)) + .isEqualTo(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled) verify(walletController) .startQuickAccessUiIntent( activityStarter, diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt index b4d5464d1177..7116cc101d3f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt @@ -25,11 +25,12 @@ import com.android.systemui.animation.ActivityLaunchAnimator import com.android.systemui.animation.Expandable import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon +import com.android.systemui.keyguard.data.quickaffordance.BuiltInKeyguardQuickAffordanceKeys +import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository -import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordancePosition -import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceConfig import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceRegistry -import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition import com.android.systemui.plugins.ActivityStarter import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.policy.KeyguardStateController @@ -211,7 +212,11 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) whenever(expandable.activityLaunchController()).thenReturn(animationController) - homeControls = object : FakeKeyguardQuickAffordanceConfig() {} + homeControls = + object : + FakeKeyguardQuickAffordanceConfig( + BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS + ) {} underTest = KeyguardQuickAffordanceInteractor( keyguardInteractor = KeyguardInteractor(repository = FakeKeyguardRepository()), @@ -224,8 +229,14 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() { ), KeyguardQuickAffordancePosition.BOTTOM_END to listOf( - object : FakeKeyguardQuickAffordanceConfig() {}, - object : FakeKeyguardQuickAffordanceConfig() {}, + object : + FakeKeyguardQuickAffordanceConfig( + BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET + ) {}, + object : + FakeKeyguardQuickAffordanceConfig( + BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER + ) {}, ), ), ), @@ -237,30 +248,30 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() { } @Test - fun onQuickAffordanceClicked() = runBlockingTest { + fun onQuickAffordanceTriggered() = runBlockingTest { setUpMocks( needStrongAuthAfterBoot = needStrongAuthAfterBoot, keyguardIsUnlocked = keyguardIsUnlocked, ) homeControls.setState( - state = - KeyguardQuickAffordanceConfig.State.Visible( + lockScreenState = + KeyguardQuickAffordanceConfig.LockScreenState.Visible( icon = DRAWABLE, ) ) - homeControls.onClickedResult = + homeControls.onTriggeredResult = if (startActivity) { - KeyguardQuickAffordanceConfig.OnClickedResult.StartActivity( + KeyguardQuickAffordanceConfig.OnTriggeredResult.StartActivity( intent = INTENT, canShowWhileLocked = canShowWhileLocked, ) } else { - KeyguardQuickAffordanceConfig.OnClickedResult.Handled + KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled } - underTest.onQuickAffordanceClicked( - configKey = homeControls::class, + underTest.onQuickAffordanceTriggered( + configKey = BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS, expandable = expandable, ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt index 65fd6e576650..ae32ba6676be 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt @@ -22,13 +22,14 @@ import com.android.internal.widget.LockPatternUtils import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon +import com.android.systemui.keyguard.data.quickaffordance.BuiltInKeyguardQuickAffordanceKeys +import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordanceModel -import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordancePosition -import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceConfig import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceRegistry -import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceConfig -import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordanceToggleState +import com.android.systemui.keyguard.shared.quickaffordance.ActivationState +import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition import com.android.systemui.plugins.ActivityStarter import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.policy.KeyguardStateController @@ -69,9 +70,21 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { repository = FakeKeyguardRepository() repository.setKeyguardShowing(true) - homeControls = object : FakeKeyguardQuickAffordanceConfig() {} - quickAccessWallet = object : FakeKeyguardQuickAffordanceConfig() {} - qrCodeScanner = object : FakeKeyguardQuickAffordanceConfig() {} + homeControls = + object : + FakeKeyguardQuickAffordanceConfig( + BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS + ) {} + quickAccessWallet = + object : + FakeKeyguardQuickAffordanceConfig( + BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET + ) {} + qrCodeScanner = + object : + FakeKeyguardQuickAffordanceConfig( + BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER + ) {} underTest = KeyguardQuickAffordanceInteractor( @@ -99,11 +112,11 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { @Test fun `quickAffordance - bottom start affordance is visible`() = runBlockingTest { - val configKey = homeControls::class + val configKey = BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS homeControls.setState( - KeyguardQuickAffordanceConfig.State.Visible( + KeyguardQuickAffordanceConfig.LockScreenState.Visible( icon = ICON, - toggle = KeyguardQuickAffordanceToggleState.On, + activationState = ActivationState.Active, ) ) @@ -124,15 +137,15 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { assertThat(visibleModel.icon).isEqualTo(ICON) assertThat(visibleModel.icon.contentDescription) .isEqualTo(ContentDescription.Resource(res = CONTENT_DESCRIPTION_RESOURCE_ID)) - assertThat(visibleModel.toggle).isEqualTo(KeyguardQuickAffordanceToggleState.On) + assertThat(visibleModel.activationState).isEqualTo(ActivationState.Active) job.cancel() } @Test fun `quickAffordance - bottom end affordance is visible`() = runBlockingTest { - val configKey = quickAccessWallet::class + val configKey = BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET quickAccessWallet.setState( - KeyguardQuickAffordanceConfig.State.Visible( + KeyguardQuickAffordanceConfig.LockScreenState.Visible( icon = ICON, ) ) @@ -154,7 +167,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { assertThat(visibleModel.icon).isEqualTo(ICON) assertThat(visibleModel.icon.contentDescription) .isEqualTo(ContentDescription.Resource(res = CONTENT_DESCRIPTION_RESOURCE_ID)) - assertThat(visibleModel.toggle).isEqualTo(KeyguardQuickAffordanceToggleState.NotSupported) + assertThat(visibleModel.activationState).isEqualTo(ActivationState.NotSupported) job.cancel() } @@ -162,7 +175,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { fun `quickAffordance - bottom start affordance hidden while dozing`() = runBlockingTest { repository.setDozing(true) homeControls.setState( - KeyguardQuickAffordanceConfig.State.Visible( + KeyguardQuickAffordanceConfig.LockScreenState.Visible( icon = ICON, ) ) @@ -182,7 +195,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { runBlockingTest { repository.setKeyguardShowing(false) homeControls.setState( - KeyguardQuickAffordanceConfig.State.Visible( + KeyguardQuickAffordanceConfig.LockScreenState.Visible( icon = ICON, ) ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/FakeKeyguardQuickAffordanceRegistry.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/FakeKeyguardQuickAffordanceRegistry.kt index e68c43f4abd7..13e2768e1fd0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/FakeKeyguardQuickAffordanceRegistry.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/FakeKeyguardQuickAffordanceRegistry.kt @@ -17,8 +17,8 @@ package com.android.systemui.keyguard.domain.quickaffordance -import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordancePosition -import kotlin.reflect.KClass +import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition /** Fake implementation of [FakeKeyguardQuickAffordanceRegistry], for tests. */ class FakeKeyguardQuickAffordanceRegistry( @@ -33,11 +33,8 @@ class FakeKeyguardQuickAffordanceRegistry( } override fun get( - configClass: KClass<out FakeKeyguardQuickAffordanceConfig> + key: String, ): FakeKeyguardQuickAffordanceConfig { - return configsByPosition.values - .flatten() - .associateBy { config -> config::class } - .getValue(configClass) + return configsByPosition.values.flatten().associateBy { config -> config.key }.getValue(key) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt index d674c89c0e14..f73d1ecf9373 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt @@ -23,15 +23,16 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.animation.Expandable import com.android.systemui.common.shared.model.Icon import com.android.systemui.doze.util.BurnInHelperWrapper +import com.android.systemui.keyguard.data.quickaffordance.BuiltInKeyguardQuickAffordanceKeys +import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor -import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordancePosition -import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceConfig import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceRegistry -import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceConfig -import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordanceToggleState +import com.android.systemui.keyguard.shared.quickaffordance.ActivationState +import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition import com.android.systemui.plugins.ActivityStarter import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.policy.KeyguardStateController @@ -40,7 +41,6 @@ import com.android.systemui.util.mockito.mock import com.google.common.truth.Truth.assertThat import kotlin.math.max import kotlin.math.min -import kotlin.reflect.KClass import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.test.runBlockingTest @@ -81,9 +81,21 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { whenever(burnInHelperWrapper.burnInOffset(anyInt(), any())) .thenReturn(RETURNED_BURN_IN_OFFSET) - homeControlsQuickAffordanceConfig = object : FakeKeyguardQuickAffordanceConfig() {} - quickAccessWalletAffordanceConfig = object : FakeKeyguardQuickAffordanceConfig() {} - qrCodeScannerAffordanceConfig = object : FakeKeyguardQuickAffordanceConfig() {} + homeControlsQuickAffordanceConfig = + object : + FakeKeyguardQuickAffordanceConfig( + BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS + ) {} + quickAccessWalletAffordanceConfig = + object : + FakeKeyguardQuickAffordanceConfig( + BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET + ) {} + qrCodeScannerAffordanceConfig = + object : + FakeKeyguardQuickAffordanceConfig( + BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER + ) {} registry = FakeKeyguardQuickAffordanceRegistry( mapOf( @@ -489,42 +501,42 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { private suspend fun setUpQuickAffordanceModel( position: KeyguardQuickAffordancePosition, testConfig: TestConfig, - ): KClass<out FakeKeyguardQuickAffordanceConfig> { + ): String { val config = when (position) { KeyguardQuickAffordancePosition.BOTTOM_START -> homeControlsQuickAffordanceConfig KeyguardQuickAffordancePosition.BOTTOM_END -> quickAccessWalletAffordanceConfig } - val state = + val lockScreenState = if (testConfig.isVisible) { if (testConfig.intent != null) { - config.onClickedResult = - KeyguardQuickAffordanceConfig.OnClickedResult.StartActivity( + config.onTriggeredResult = + KeyguardQuickAffordanceConfig.OnTriggeredResult.StartActivity( intent = testConfig.intent, canShowWhileLocked = testConfig.canShowWhileLocked, ) } - KeyguardQuickAffordanceConfig.State.Visible( + KeyguardQuickAffordanceConfig.LockScreenState.Visible( icon = testConfig.icon ?: error("Icon is unexpectedly null!"), - toggle = + activationState = when (testConfig.isActivated) { - true -> KeyguardQuickAffordanceToggleState.On - false -> KeyguardQuickAffordanceToggleState.Off - null -> KeyguardQuickAffordanceToggleState.NotSupported + true -> ActivationState.Active + false -> ActivationState.Inactive + null -> ActivationState.NotSupported } ) } else { - KeyguardQuickAffordanceConfig.State.Hidden + KeyguardQuickAffordanceConfig.LockScreenState.Hidden } - config.setState(state) - return config::class + config.setState(lockScreenState) + return config.key } private fun assertQuickAffordanceViewModel( viewModel: KeyguardQuickAffordanceViewModel?, testConfig: TestConfig, - configKey: KClass<out FakeKeyguardQuickAffordanceConfig>, + configKey: String, ) { checkNotNull(viewModel) assertThat(viewModel.isVisible).isEqualTo(testConfig.isVisible) diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt index 071604dc5790..920801f95f5b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt @@ -31,7 +31,7 @@ import com.android.systemui.dreams.DreamOverlayStateController import com.android.systemui.keyguard.WakefulnessLifecycle import com.android.systemui.media.dream.MediaDreamComplication import com.android.systemui.plugins.statusbar.StatusBarStateController -import com.android.systemui.shade.testing.FakeNotifPanelEvents +import com.android.systemui.shade.ShadeExpansionStateManager import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.SysuiStatusBarStateController import com.android.systemui.statusbar.phone.KeyguardBypassController @@ -89,7 +89,7 @@ class MediaHierarchyManagerTest : SysuiTestCase() { private lateinit var mediaHierarchyManager: MediaHierarchyManager private lateinit var mediaFrame: ViewGroup private val configurationController = FakeConfigurationController() - private val notifPanelEvents = FakeNotifPanelEvents() + private val notifPanelEvents = ShadeExpansionStateManager() private val settings = FakeSettings() private lateinit var testableLooper: TestableLooper private lateinit var fakeHandler: FakeHandler @@ -346,7 +346,7 @@ class MediaHierarchyManagerTest : SysuiTestCase() { @Test fun isCurrentlyInGuidedTransformation_hostsVisible_expandImmediateEnabled_returnsFalse() { - notifPanelEvents.changeExpandImmediate(expandImmediate = true) + notifPanelEvents.notifyExpandImmediateChange(true) goToLockscreen() enterGuidedTransformation() whenever(lockHost.visible).thenReturn(true) diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt index fdeb3f5eb857..ad19bc2a80e0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt @@ -45,6 +45,7 @@ import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator +import com.android.systemui.temporarydisplay.chipbar.ChipbarLogger import com.android.systemui.temporarydisplay.chipbar.FakeChipbarCoordinator import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.any @@ -80,6 +81,7 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { @Mock private lateinit var configurationController: ConfigurationController @Mock private lateinit var falsingManager: FalsingManager @Mock private lateinit var falsingCollector: FalsingCollector + @Mock private lateinit var chipbarLogger: ChipbarLogger @Mock private lateinit var logger: MediaTttLogger @Mock private lateinit var mediaTttFlags: MediaTttFlags @Mock private lateinit var packageManager: PackageManager @@ -122,7 +124,7 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { chipbarCoordinator = FakeChipbarCoordinator( context, - logger, + chipbarLogger, windowManager, fakeExecutor, accessibilityManager, diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt new file mode 100644 index 000000000000..f20c6a29b840 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.notetask + +import android.app.KeyguardManager +import android.content.Context +import android.content.Intent +import android.os.UserManager +import android.test.suitebuilder.annotation.SmallTest +import android.view.KeyEvent +import androidx.test.runner.AndroidJUnit4 +import com.android.systemui.SysuiTestCase +import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.NOTES_ACTION +import com.android.systemui.util.mockito.whenever +import com.android.wm.shell.floating.FloatingTasks +import java.util.* +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +/** + * Tests for [NoteTaskController]. + * + * Build/Install/Run: + * - atest SystemUITests:NoteTaskControllerTest + */ +@SmallTest +@RunWith(AndroidJUnit4::class) +internal class NoteTaskControllerTest : SysuiTestCase() { + + private val notesIntent = Intent(NOTES_ACTION) + + @Mock lateinit var context: Context + @Mock lateinit var noteTaskIntentResolver: NoteTaskIntentResolver + @Mock lateinit var floatingTasks: FloatingTasks + @Mock lateinit var optionalFloatingTasks: Optional<FloatingTasks> + @Mock lateinit var keyguardManager: KeyguardManager + @Mock lateinit var optionalKeyguardManager: Optional<KeyguardManager> + @Mock lateinit var optionalUserManager: Optional<UserManager> + @Mock lateinit var userManager: UserManager + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + whenever(noteTaskIntentResolver.resolveIntent()).thenReturn(notesIntent) + whenever(optionalFloatingTasks.orElse(null)).thenReturn(floatingTasks) + whenever(optionalKeyguardManager.orElse(null)).thenReturn(keyguardManager) + whenever(optionalUserManager.orElse(null)).thenReturn(userManager) + whenever(userManager.isUserUnlocked).thenReturn(true) + } + + private fun createNoteTaskController(isEnabled: Boolean = true): NoteTaskController { + return NoteTaskController( + context = context, + intentResolver = noteTaskIntentResolver, + optionalFloatingTasks = optionalFloatingTasks, + optionalKeyguardManager = optionalKeyguardManager, + optionalUserManager = optionalUserManager, + isEnabled = isEnabled, + ) + } + + @Test + fun handleSystemKey_keyguardIsLocked_shouldStartActivity() { + whenever(keyguardManager.isKeyguardLocked).thenReturn(true) + + createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1) + + verify(context).startActivity(notesIntent) + verify(floatingTasks, never()).showOrSetStashed(notesIntent) + } + + @Test + fun handleSystemKey_keyguardIsUnlocked_shouldStartFloatingTask() { + whenever(keyguardManager.isKeyguardLocked).thenReturn(false) + + createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1) + + verify(floatingTasks).showOrSetStashed(notesIntent) + verify(context, never()).startActivity(notesIntent) + } + + @Test + fun handleSystemKey_receiveInvalidSystemKey_shouldDoNothing() { + createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_UNKNOWN) + + verify(context, never()).startActivity(notesIntent) + verify(floatingTasks, never()).showOrSetStashed(notesIntent) + } + + @Test + fun handleSystemKey_floatingTasksIsNull_shouldDoNothing() { + whenever(optionalFloatingTasks.orElse(null)).thenReturn(null) + + createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1) + + verify(context, never()).startActivity(notesIntent) + verify(floatingTasks, never()).showOrSetStashed(notesIntent) + } + + @Test + fun handleSystemKey_keyguardManagerIsNull_shouldDoNothing() { + whenever(optionalKeyguardManager.orElse(null)).thenReturn(null) + + createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1) + + verify(context, never()).startActivity(notesIntent) + verify(floatingTasks, never()).showOrSetStashed(notesIntent) + } + + @Test + fun handleSystemKey_userManagerIsNull_shouldDoNothing() { + whenever(optionalUserManager.orElse(null)).thenReturn(null) + + createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1) + + verify(context, never()).startActivity(notesIntent) + verify(floatingTasks, never()).showOrSetStashed(notesIntent) + } + + @Test + fun handleSystemKey_intentResolverReturnsNull_shouldDoNothing() { + whenever(noteTaskIntentResolver.resolveIntent()).thenReturn(null) + + createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1) + + verify(context, never()).startActivity(notesIntent) + verify(floatingTasks, never()).showOrSetStashed(notesIntent) + } + + @Test + fun handleSystemKey_flagDisabled_shouldDoNothing() { + createNoteTaskController(isEnabled = false).handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1) + + verify(context, never()).startActivity(notesIntent) + verify(floatingTasks, never()).showOrSetStashed(notesIntent) + } + + @Test + fun handleSystemKey_userIsLocked_shouldDoNothing() { + whenever(userManager.isUserUnlocked).thenReturn(false) + + createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1) + + verify(context, never()).startActivity(notesIntent) + verify(floatingTasks, never()).showOrSetStashed(notesIntent) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt new file mode 100644 index 000000000000..f344c8d9eec4 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.notetask + +import android.test.suitebuilder.annotation.SmallTest +import androidx.test.runner.AndroidJUnit4 +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.CommandQueue +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import com.android.wm.shell.floating.FloatingTasks +import java.util.* +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.any +import org.mockito.Mock +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +/** + * Tests for [NoteTaskController]. + * + * Build/Install/Run: + * - atest SystemUITests:NoteTaskInitializerTest + */ +@SmallTest +@RunWith(AndroidJUnit4::class) +internal class NoteTaskInitializerTest : SysuiTestCase() { + + @Mock lateinit var commandQueue: CommandQueue + @Mock lateinit var floatingTasks: FloatingTasks + @Mock lateinit var optionalFloatingTasks: Optional<FloatingTasks> + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + whenever(optionalFloatingTasks.isPresent).thenReturn(true) + whenever(optionalFloatingTasks.orElse(null)).thenReturn(floatingTasks) + } + + private fun createNoteTaskInitializer(isEnabled: Boolean = true): NoteTaskInitializer { + return NoteTaskInitializer( + optionalFloatingTasks = optionalFloatingTasks, + lazyNoteTaskController = mock(), + commandQueue = commandQueue, + isEnabled = isEnabled, + ) + } + + @Test + fun initialize_shouldAddCallbacks() { + createNoteTaskInitializer().initialize() + + verify(commandQueue).addCallback(any()) + } + + @Test + fun initialize_flagDisabled_shouldDoNothing() { + createNoteTaskInitializer(isEnabled = false).initialize() + + verify(commandQueue, never()).addCallback(any()) + } + + @Test + fun initialize_floatingTasksNotPresent_shouldDoNothing() { + whenever(optionalFloatingTasks.isPresent).thenReturn(false) + + createNoteTaskInitializer().initialize() + + verify(commandQueue, never()).addCallback(any()) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskIntentResolverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskIntentResolverTest.kt new file mode 100644 index 000000000000..dd2cc2ffc9db --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskIntentResolverTest.kt @@ -0,0 +1,222 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.notetask + +import android.content.ComponentName +import android.content.Intent +import android.content.pm.ActivityInfo +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.content.pm.PackageManager.ResolveInfoFlags +import android.content.pm.ResolveInfo +import android.content.pm.ServiceInfo +import android.test.suitebuilder.annotation.SmallTest +import androidx.test.runner.AndroidJUnit4 +import com.android.systemui.SysuiTestCase +import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.NOTES_ACTION +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.any +import org.mockito.MockitoAnnotations + +/** + * Tests for [NoteTaskIntentResolver]. + * + * Build/Install/Run: + * - atest SystemUITests:NoteTaskIntentResolverTest + */ +@SmallTest +@RunWith(AndroidJUnit4::class) +internal class NoteTaskIntentResolverTest : SysuiTestCase() { + + @Mock lateinit var packageManager: PackageManager + + private lateinit var resolver: NoteTaskIntentResolver + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + resolver = NoteTaskIntentResolver(packageManager) + } + + private fun createResolveInfo( + packageName: String = "PackageName", + activityInfo: ActivityInfo? = null, + ): ResolveInfo { + return ResolveInfo().apply { + serviceInfo = + ServiceInfo().apply { + applicationInfo = ApplicationInfo().apply { this.packageName = packageName } + } + this.activityInfo = activityInfo + } + } + + private fun createActivityInfo( + name: String? = "ActivityName", + exported: Boolean = true, + enabled: Boolean = true, + showWhenLocked: Boolean = true, + turnScreenOn: Boolean = true, + ): ActivityInfo { + return ActivityInfo().apply { + this.name = name + this.exported = exported + this.enabled = enabled + if (showWhenLocked) { + flags = flags or ActivityInfo.FLAG_SHOW_WHEN_LOCKED + } + if (turnScreenOn) { + flags = flags or ActivityInfo.FLAG_TURN_SCREEN_ON + } + } + } + + private fun givenQueryIntentActivities(block: () -> List<ResolveInfo>) { + whenever(packageManager.queryIntentActivities(any(), any<ResolveInfoFlags>())) + .thenReturn(block()) + } + + private fun givenResolveActivity(block: () -> ResolveInfo?) { + whenever(packageManager.resolveActivity(any(), any<ResolveInfoFlags>())).thenReturn(block()) + } + + @Test + fun resolveIntent_shouldReturnNotesIntent() { + givenQueryIntentActivities { listOf(createResolveInfo()) } + givenResolveActivity { createResolveInfo(activityInfo = createActivityInfo()) } + + val actual = resolver.resolveIntent() + + val expected = + Intent(NOTES_ACTION) + .setComponent(ComponentName("PackageName", "ActivityName")) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + // Compares the string representation of both intents, as they are different instances. + assertThat(actual.toString()).isEqualTo(expected.toString()) + } + + @Test + fun resolveIntent_activityInfoEnabledIsFalse_shouldReturnNull() { + givenQueryIntentActivities { listOf(createResolveInfo()) } + givenResolveActivity { + createResolveInfo(activityInfo = createActivityInfo(enabled = false)) + } + + val actual = resolver.resolveIntent() + + assertThat(actual).isNull() + } + + @Test + fun resolveIntent_activityInfoExportedIsFalse_shouldReturnNull() { + givenQueryIntentActivities { listOf(createResolveInfo()) } + givenResolveActivity { + createResolveInfo(activityInfo = createActivityInfo(exported = false)) + } + + val actual = resolver.resolveIntent() + + assertThat(actual).isNull() + } + + @Test + fun resolveIntent_activityInfoShowWhenLockedIsFalse_shouldReturnNull() { + givenQueryIntentActivities { listOf(createResolveInfo()) } + givenResolveActivity { + createResolveInfo(activityInfo = createActivityInfo(showWhenLocked = false)) + } + + val actual = resolver.resolveIntent() + + assertThat(actual).isNull() + } + + @Test + fun resolveIntent_activityInfoTurnScreenOnIsFalse_shouldReturnNull() { + givenQueryIntentActivities { listOf(createResolveInfo()) } + givenResolveActivity { + createResolveInfo(activityInfo = createActivityInfo(turnScreenOn = false)) + } + + val actual = resolver.resolveIntent() + + assertThat(actual).isNull() + } + + @Test + fun resolveIntent_activityInfoNameIsBlank_shouldReturnNull() { + givenQueryIntentActivities { listOf(createResolveInfo()) } + givenResolveActivity { createResolveInfo(activityInfo = createActivityInfo(name = "")) } + + val actual = resolver.resolveIntent() + + assertThat(actual).isNull() + } + + @Test + fun resolveIntent_activityInfoNameIsNull_shouldReturnNull() { + givenQueryIntentActivities { listOf(createResolveInfo()) } + givenResolveActivity { createResolveInfo(activityInfo = createActivityInfo(name = null)) } + + val actual = resolver.resolveIntent() + + assertThat(actual).isNull() + } + + @Test + fun resolveIntent_activityInfoIsNull_shouldReturnNull() { + givenQueryIntentActivities { listOf(createResolveInfo()) } + givenResolveActivity { createResolveInfo(activityInfo = null) } + + val actual = resolver.resolveIntent() + + assertThat(actual).isNull() + } + + @Test + fun resolveIntent_resolveActivityIsNull_shouldReturnNull() { + givenQueryIntentActivities { listOf(createResolveInfo()) } + givenResolveActivity { null } + + val actual = resolver.resolveIntent() + + assertThat(actual).isNull() + } + + @Test + fun resolveIntent_packageNameIsBlank_shouldReturnNull() { + givenQueryIntentActivities { listOf(createResolveInfo(packageName = "")) } + + val actual = resolver.resolveIntent() + + assertThat(actual).isNull() + } + + @Test + fun resolveIntent_activityNotFoundForAction_shouldReturnNull() { + givenQueryIntentActivities { emptyList() } + + val actual = resolver.resolveIntent() + + assertThat(actual).isNull() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java index 02f28a235b95..ac4dd49208c2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java @@ -438,8 +438,6 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { when(mSysUiState.setFlag(anyInt(), anyBoolean())).thenReturn(mSysUiState); mMainHandler = new Handler(Looper.getMainLooper()); - NotificationPanelViewController.PanelEventsEmitter panelEventsEmitter = - new NotificationPanelViewController.PanelEventsEmitter(); mNotificationPanelViewController = new NotificationPanelViewController( mView, @@ -495,7 +493,6 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { () -> mKeyguardBottomAreaViewController, mKeyguardUnlockAnimationController, mNotificationListContainer, - panelEventsEmitter, mNotificationStackSizeCalculator, mUnlockedScreenOffAnimationController, mShadeTransitionController, @@ -1597,7 +1594,7 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { mTouchHandler.onTouch(mock(View.class), mDownMotionEvent); mEmptySpaceClickListenerCaptor.getValue().onEmptySpaceClicked(0, 0); - verify(mUpdateMonitor).requestFaceAuth(true, + verify(mUpdateMonitor).requestFaceAuth( FaceAuthApiRequestReason.NOTIFICATION_PANEL_CLICKED); } @@ -1607,7 +1604,7 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { mNotificationPanelViewController.mStatusBarStateListener; statusBarStateListener.onStateChanged(KEYGUARD); mNotificationPanelViewController.setDozing(false, false); - when(mUpdateMonitor.requestFaceAuth(true, NOTIFICATION_PANEL_CLICKED)).thenReturn(false); + when(mUpdateMonitor.requestFaceAuth(NOTIFICATION_PANEL_CLICKED)).thenReturn(false); // This sets the dozing state that is read when onMiddleClicked is eventually invoked. mTouchHandler.onTouch(mock(View.class), mDownMotionEvent); @@ -1622,7 +1619,7 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { mNotificationPanelViewController.mStatusBarStateListener; statusBarStateListener.onStateChanged(KEYGUARD); mNotificationPanelViewController.setDozing(false, false); - when(mUpdateMonitor.requestFaceAuth(true, NOTIFICATION_PANEL_CLICKED)).thenReturn(true); + when(mUpdateMonitor.requestFaceAuth(NOTIFICATION_PANEL_CLICKED)).thenReturn(true); // This sets the dozing state that is read when onMiddleClicked is eventually invoked. mTouchHandler.onTouch(mock(View.class), mDownMotionEvent); @@ -1642,7 +1639,7 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { mTouchHandler.onTouch(mock(View.class), mDownMotionEvent); mEmptySpaceClickListenerCaptor.getValue().onEmptySpaceClicked(0, 0); - verify(mUpdateMonitor, never()).requestFaceAuth(anyBoolean(), anyString()); + verify(mUpdateMonitor, never()).requestFaceAuth(anyString()); } @Test @@ -1653,7 +1650,7 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { mEmptySpaceClickListenerCaptor.getValue().onEmptySpaceClicked(0, 0); - verify(mUpdateMonitor, never()).requestFaceAuth(anyBoolean(), anyString()); + verify(mUpdateMonitor, never()).requestFaceAuth(anyString()); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/testing/FakeNotifPanelEvents.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/testing/FakeNotifPanelEvents.kt deleted file mode 100644 index d05213877232..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/testing/FakeNotifPanelEvents.kt +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.shade.testing - -import com.android.systemui.shade.NotifPanelEvents - -/** Fake implementation of [NotifPanelEvents] for testing. */ -class FakeNotifPanelEvents : NotifPanelEvents { - - private val listeners = mutableListOf<NotifPanelEvents.Listener>() - - override fun registerListener(listener: NotifPanelEvents.Listener) { - listeners.add(listener) - } - - override fun unregisterListener(listener: NotifPanelEvents.Listener) { - listeners.remove(listener) - } - - fun changeExpandImmediate(expandImmediate: Boolean) { - listeners.forEach { it.onExpandImmediateChanged(expandImmediate) } - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/system/RemoteTransitionTest.java b/packages/SystemUI/tests/src/com/android/systemui/shared/system/RemoteTransitionTest.java index 64dc9568030b..4478039912c8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shared/system/RemoteTransitionTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/system/RemoteTransitionTest.java @@ -25,6 +25,7 @@ import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_OPEN; import static android.window.TransitionInfo.FLAG_FIRST_CUSTOM; +import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY; import static android.window.TransitionInfo.FLAG_IS_WALLPAPER; import static android.window.TransitionInfo.FLAG_SHOW_WALLPAPER; import static android.window.TransitionInfo.FLAG_TRANSLUCENT; @@ -69,10 +70,13 @@ public class RemoteTransitionTest extends SysuiTestCase { TransitionInfo combined = new TransitionInfoBuilder(TRANSIT_CLOSE) .addChange(TRANSIT_CHANGE, FLAG_SHOW_WALLPAPER, createTaskInfo(1 /* taskId */, ACTIVITY_TYPE_STANDARD)) + // Embedded TaskFragment should be excluded when animated with Task. + .addChange(TRANSIT_CLOSE, FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY, null /* taskInfo */) .addChange(TRANSIT_CLOSE, 0 /* flags */, createTaskInfo(2 /* taskId */, ACTIVITY_TYPE_STANDARD)) .addChange(TRANSIT_OPEN, FLAG_IS_WALLPAPER, null /* taskInfo */) - .addChange(TRANSIT_CHANGE, FLAG_FIRST_CUSTOM, null /* taskInfo */).build(); + .addChange(TRANSIT_CHANGE, FLAG_FIRST_CUSTOM, null /* taskInfo */) + .build(); // Check apps extraction RemoteAnimationTarget[] wrapped = RemoteAnimationTargetCompat.wrapApps(combined, mock(SurfaceControl.Transaction.class), null /* leashes */); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorTest.java index c961cec39208..b4a5f5ce205f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorTest.java @@ -36,7 +36,8 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.dump.DumpManager; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.plugins.statusbar.StatusBarStateController; -import com.android.systemui.shade.NotifPanelEvents; +import com.android.systemui.shade.ShadeStateEvents; +import com.android.systemui.shade.ShadeStateEvents.ShadeStateEventsListener; import com.android.systemui.statusbar.notification.collection.GroupEntry; import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder; import com.android.systemui.statusbar.notification.collection.NotifPipeline; @@ -71,12 +72,12 @@ public class VisualStabilityCoordinatorTest extends SysuiTestCase { @Mock private StatusBarStateController mStatusBarStateController; @Mock private Pluggable.PluggableListener<NotifStabilityManager> mInvalidateListener; @Mock private HeadsUpManager mHeadsUpManager; - @Mock private NotifPanelEvents mNotifPanelEvents; + @Mock private ShadeStateEvents mShadeStateEvents; @Mock private VisualStabilityProvider mVisualStabilityProvider; @Captor private ArgumentCaptor<WakefulnessLifecycle.Observer> mWakefulnessObserverCaptor; @Captor private ArgumentCaptor<StatusBarStateController.StateListener> mSBStateListenerCaptor; - @Captor private ArgumentCaptor<NotifPanelEvents.Listener> mNotifPanelEventsCallbackCaptor; + @Captor private ArgumentCaptor<ShadeStateEventsListener> mNotifPanelEventsCallbackCaptor; @Captor private ArgumentCaptor<NotifStabilityManager> mNotifStabilityManagerCaptor; private FakeSystemClock mFakeSystemClock = new FakeSystemClock(); @@ -84,7 +85,7 @@ public class VisualStabilityCoordinatorTest extends SysuiTestCase { private WakefulnessLifecycle.Observer mWakefulnessObserver; private StatusBarStateController.StateListener mStatusBarStateListener; - private NotifPanelEvents.Listener mNotifPanelEventsCallback; + private ShadeStateEvents.ShadeStateEventsListener mNotifPanelEventsCallback; private NotifStabilityManager mNotifStabilityManager; private NotificationEntry mEntry; private GroupEntry mGroupEntry; @@ -97,7 +98,7 @@ public class VisualStabilityCoordinatorTest extends SysuiTestCase { mFakeExecutor, mDumpManager, mHeadsUpManager, - mNotifPanelEvents, + mShadeStateEvents, mStatusBarStateController, mVisualStabilityProvider, mWakefulnessLifecycle); @@ -111,7 +112,8 @@ public class VisualStabilityCoordinatorTest extends SysuiTestCase { verify(mStatusBarStateController).addCallback(mSBStateListenerCaptor.capture()); mStatusBarStateListener = mSBStateListenerCaptor.getValue(); - verify(mNotifPanelEvents).registerListener(mNotifPanelEventsCallbackCaptor.capture()); + verify(mShadeStateEvents).addShadeStateEventsListener( + mNotifPanelEventsCallbackCaptor.capture()); mNotifPanelEventsCallback = mNotifPanelEventsCallbackCaptor.getValue(); verify(mNotifPipeline).setVisualStabilityManager(mNotifStabilityManagerCaptor.capture()); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDifferTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDifferTest.java deleted file mode 100644 index ab712649a90f..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDifferTest.java +++ /dev/null @@ -1,317 +0,0 @@ -/* - * 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 com.android.systemui.statusbar.notification.collection.render; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; - -import android.content.Context; -import android.testing.AndroidTestingRunner; -import android.view.View; -import android.widget.FrameLayout; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.test.filters.SmallTest; - -import com.android.systemui.SysuiTestCase; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import java.util.List; - -@SmallTest -@RunWith(AndroidTestingRunner.class) -public class ShadeViewDifferTest extends SysuiTestCase { - private ShadeViewDiffer mDiffer; - - private FakeController mRootController = new FakeController(mContext, "RootController"); - private FakeController mController1 = new FakeController(mContext, "Controller1"); - private FakeController mController2 = new FakeController(mContext, "Controller2"); - private FakeController mController3 = new FakeController(mContext, "Controller3"); - private FakeController mController4 = new FakeController(mContext, "Controller4"); - private FakeController mController5 = new FakeController(mContext, "Controller5"); - private FakeController mController6 = new FakeController(mContext, "Controller6"); - private FakeController mController7 = new FakeController(mContext, "Controller7"); - - @Mock - ShadeViewDifferLogger mLogger; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - - mDiffer = new ShadeViewDiffer(mRootController, mLogger); - } - - @Test - public void testAddInitialViews() { - // WHEN a spec is applied to an empty root - // THEN the final tree matches the spec - applySpecAndCheck( - node(mController1), - node(mController2, - node(mController3), - node(mController4) - ), - node(mController5) - ); - } - - @Test - public void testDetachViews() { - // GIVEN a preexisting tree of controllers - applySpecAndCheck( - node(mController1), - node(mController2, - node(mController3), - node(mController4) - ), - node(mController5) - ); - - // WHEN the new spec removes nodes - // THEN the final tree matches the spec - applySpecAndCheck( - node(mController5) - ); - } - - @Test - public void testReparentChildren() { - // GIVEN a preexisting tree of controllers - applySpecAndCheck( - node(mController1), - node(mController2, - node(mController3), - node(mController4) - ), - node(mController5) - ); - - // WHEN the parents of the controllers are all shuffled around - // THEN the final tree matches the spec - applySpecAndCheck( - node(mController1), - node(mController4), - node(mController3, - node(mController2) - ) - ); - } - - @Test - public void testReorderChildren() { - // GIVEN a preexisting tree of controllers - applySpecAndCheck( - node(mController1), - node(mController2), - node(mController3), - node(mController4) - ); - - // WHEN the children change order - // THEN the final tree matches the spec - applySpecAndCheck( - node(mController3), - node(mController2), - node(mController4), - node(mController1) - ); - } - - @Test - public void testRemovedGroupsAreBrokenApart() { - // GIVEN a preexisting tree with a group - applySpecAndCheck( - node(mController1), - node(mController2, - node(mController3), - node(mController4), - node(mController5) - ) - ); - - // WHEN the new spec removes the entire group - applySpecAndCheck( - node(mController1) - ); - - // THEN the group children are no longer attached to their parent - assertNull(mController3.getView().getParent()); - assertNull(mController4.getView().getParent()); - assertNull(mController5.getView().getParent()); - } - - @Test - public void testUnmanagedViews() { - // GIVEN a preexisting tree of controllers - applySpecAndCheck( - node(mController1), - node(mController2, - node(mController3), - node(mController4) - ), - node(mController5) - ); - - // GIVEN some additional unmanaged views attached to the tree - View unmanagedView1 = new View(mContext); - View unmanagedView2 = new View(mContext); - - mRootController.getView().addView(unmanagedView1, 1); - mController2.getView().addView(unmanagedView2, 0); - - // WHEN a new spec is applied with additional nodes - // THEN the final tree matches the spec - applySpecAndCheck( - node(mController1), - node(mController2, - node(mController3), - node(mController4), - node(mController6) - ), - node(mController5), - node(mController7) - ); - - // THEN the unmanaged views have been pushed to the end of their parents - assertEquals(unmanagedView1, mRootController.view.getChildAt(4)); - assertEquals(unmanagedView2, mController2.view.getChildAt(3)); - } - - private void applySpecAndCheck(NodeSpec spec) { - mDiffer.applySpec(spec); - checkMatchesSpec(spec); - } - - private void applySpecAndCheck(SpecBuilder... children) { - applySpecAndCheck(node(mRootController, children).build()); - } - - private void checkMatchesSpec(NodeSpec spec) { - final NodeController parent = spec.getController(); - final List<NodeSpec> children = spec.getChildren(); - - for (int i = 0; i < children.size(); i++) { - NodeSpec childSpec = children.get(i); - View view = parent.getChildAt(i); - - assertEquals( - "Child " + i + " of parent " + parent.getNodeLabel() + " should be " - + childSpec.getController().getNodeLabel() + " but is instead " - + (view != null ? mDiffer.getViewLabel(view) : "null"), - view, - childSpec.getController().getView()); - - if (!childSpec.getChildren().isEmpty()) { - checkMatchesSpec(childSpec); - } - } - } - - private static class FakeController implements NodeController { - - public final FrameLayout view; - private final String mLabel; - - FakeController(Context context, String label) { - view = new FrameLayout(context); - mLabel = label; - } - - @NonNull - @Override - public String getNodeLabel() { - return mLabel; - } - - @NonNull - @Override - public FrameLayout getView() { - return view; - } - - @Override - public int getChildCount() { - return view.getChildCount(); - } - - @Override - public View getChildAt(int index) { - return view.getChildAt(index); - } - - @Override - public void addChildAt(@NonNull NodeController child, int index) { - view.addView(child.getView(), index); - } - - @Override - public void moveChildTo(@NonNull NodeController child, int index) { - view.removeView(child.getView()); - view.addView(child.getView(), index); - } - - @Override - public void removeChild(@NonNull NodeController child, boolean isTransfer) { - view.removeView(child.getView()); - } - - @Override - public void onViewAdded() { - } - - @Override - public void onViewMoved() { - } - - @Override - public void onViewRemoved() { - } - } - - private static class SpecBuilder { - private final NodeController mController; - private final SpecBuilder[] mChildren; - - SpecBuilder(NodeController controller, SpecBuilder... children) { - mController = controller; - mChildren = children; - } - - public NodeSpec build() { - return build(null); - } - - public NodeSpec build(@Nullable NodeSpec parent) { - final NodeSpecImpl spec = new NodeSpecImpl(parent, mController); - for (SpecBuilder childBuilder : mChildren) { - spec.getChildren().add(childBuilder.build(spec)); - } - return spec; - } - } - - private static SpecBuilder node(NodeController controller, SpecBuilder... children) { - return new SpecBuilder(controller, children); - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDifferTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDifferTest.kt new file mode 100644 index 000000000000..15cf17dbcf86 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDifferTest.kt @@ -0,0 +1,234 @@ +/* + * 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 com.android.systemui.statusbar.notification.collection.render + +import android.content.Context +import android.testing.AndroidTestingRunner +import android.view.View +import android.widget.FrameLayout +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.util.mockito.mock +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class ShadeViewDifferTest : SysuiTestCase() { + private lateinit var differ: ShadeViewDiffer + private val rootController = FakeController(mContext, "RootController") + private val controller1 = FakeController(mContext, "Controller1") + private val controller2 = FakeController(mContext, "Controller2") + private val controller3 = FakeController(mContext, "Controller3") + private val controller4 = FakeController(mContext, "Controller4") + private val controller5 = FakeController(mContext, "Controller5") + private val controller6 = FakeController(mContext, "Controller6") + private val controller7 = FakeController(mContext, "Controller7") + private val logger: ShadeViewDifferLogger = mock() + + @Before + fun setUp() { + differ = ShadeViewDiffer(rootController, logger) + } + + @Test + fun testAddInitialViews() { + // WHEN a spec is applied to an empty root + // THEN the final tree matches the spec + applySpecAndCheck( + node(controller1), + node(controller2, node(controller3), node(controller4)), + node(controller5) + ) + } + + @Test + fun testDetachViews() { + // GIVEN a preexisting tree of controllers + applySpecAndCheck( + node(controller1), + node(controller2, node(controller3), node(controller4)), + node(controller5) + ) + + // WHEN the new spec removes nodes + // THEN the final tree matches the spec + applySpecAndCheck(node(controller5)) + } + + @Test + fun testReparentChildren() { + // GIVEN a preexisting tree of controllers + applySpecAndCheck( + node(controller1), + node(controller2, node(controller3), node(controller4)), + node(controller5) + ) + + // WHEN the parents of the controllers are all shuffled around + // THEN the final tree matches the spec + applySpecAndCheck( + node(controller1), + node(controller4), + node(controller3, node(controller2)) + ) + } + + @Test + fun testReorderChildren() { + // GIVEN a preexisting tree of controllers + applySpecAndCheck( + node(controller1), + node(controller2), + node(controller3), + node(controller4) + ) + + // WHEN the children change order + // THEN the final tree matches the spec + applySpecAndCheck( + node(controller3), + node(controller2), + node(controller4), + node(controller1) + ) + } + + @Test + fun testRemovedGroupsAreBrokenApart() { + // GIVEN a preexisting tree with a group + applySpecAndCheck( + node(controller1), + node(controller2, node(controller3), node(controller4), node(controller5)) + ) + + // WHEN the new spec removes the entire group + applySpecAndCheck(node(controller1)) + + // THEN the group children are no longer attached to their parent + Assert.assertNull(controller3.view.parent) + Assert.assertNull(controller4.view.parent) + Assert.assertNull(controller5.view.parent) + } + + @Test + fun testUnmanagedViews() { + // GIVEN a preexisting tree of controllers + applySpecAndCheck( + node(controller1), + node(controller2, node(controller3), node(controller4)), + node(controller5) + ) + + // GIVEN some additional unmanaged views attached to the tree + val unmanagedView1 = View(mContext) + val unmanagedView2 = View(mContext) + rootController.view.addView(unmanagedView1, 1) + controller2.view.addView(unmanagedView2, 0) + + // WHEN a new spec is applied with additional nodes + // THEN the final tree matches the spec + applySpecAndCheck( + node(controller1), + node(controller2, node(controller3), node(controller4), node(controller6)), + node(controller5), + node(controller7) + ) + + // THEN the unmanaged views have been pushed to the end of their parents + Assert.assertEquals(unmanagedView1, rootController.view.getChildAt(4)) + Assert.assertEquals(unmanagedView2, controller2.view.getChildAt(3)) + } + + private fun applySpecAndCheck(spec: NodeSpec) { + differ.applySpec(spec) + checkMatchesSpec(spec) + } + + private fun applySpecAndCheck(vararg children: SpecBuilder) { + applySpecAndCheck(node(rootController, *children).build()) + } + + private fun checkMatchesSpec(spec: NodeSpec) { + val parent = spec.controller + val children = spec.children + for (i in children.indices) { + val childSpec = children[i] + val view = parent.getChildAt(i) + Assert.assertEquals( + "Child $i of parent ${parent.nodeLabel} " + + "should be ${childSpec.controller.nodeLabel} " + + "but instead " + + view?.let(differ::getViewLabel), + view, + childSpec.controller.view + ) + if (childSpec.children.isNotEmpty()) { + checkMatchesSpec(childSpec) + } + } + } + + private class FakeController(context: Context, label: String) : NodeController { + override val view: FrameLayout = FrameLayout(context) + override val nodeLabel: String = label + override fun getChildCount(): Int = view.childCount + + override fun getChildAt(index: Int): View? { + return view.getChildAt(index) + } + + override fun addChildAt(child: NodeController, index: Int) { + view.addView(child.view, index) + } + + override fun moveChildTo(child: NodeController, index: Int) { + view.removeView(child.view) + view.addView(child.view, index) + } + + override fun removeChild(child: NodeController, isTransfer: Boolean) { + view.removeView(child.view) + } + + override fun onViewAdded() {} + override fun onViewMoved() {} + override fun onViewRemoved() {} + } + + private class SpecBuilder( + private val mController: NodeController, + private val children: Array<out SpecBuilder> + ) { + + @JvmOverloads + fun build(parent: NodeSpec? = null): NodeSpec { + val spec = NodeSpecImpl(parent, mController) + for (childBuilder in children) { + spec.children.add(childBuilder.build(spec)) + } + return spec + } + } + + companion object { + private fun node(controller: NodeController, vararg children: SpecBuilder): SpecBuilder { + return SpecBuilder(controller, children) + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java index 91aecd8cf753..dceb4ff48125 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java @@ -78,6 +78,7 @@ import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController import org.junit.Assert; import org.junit.Before; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -89,6 +90,7 @@ import org.mockito.junit.MockitoRule; /** * Tests for {@link NotificationStackScrollLayout}. */ +@Ignore("b/255552856") @SmallTest @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/SystemBarAttributesListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/SystemBarAttributesListenerTest.kt index fa7b2599c108..9957c2a7f4a0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/SystemBarAttributesListenerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/SystemBarAttributesListenerTest.kt @@ -14,8 +14,6 @@ import com.android.internal.statusbar.LetterboxDetails import com.android.internal.view.AppearanceRegion import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager -import com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.Flags import com.android.systemui.statusbar.SysuiStatusBarStateController import org.junit.Before import org.junit.Test @@ -40,7 +38,6 @@ class SystemBarAttributesListenerTest : SysuiTestCase() { @Mock private lateinit var lightBarController: LightBarController @Mock private lateinit var statusBarStateController: SysuiStatusBarStateController @Mock private lateinit var letterboxAppearanceCalculator: LetterboxAppearanceCalculator - @Mock private lateinit var featureFlags: FeatureFlags @Mock private lateinit var centralSurfaces: CentralSurfaces private lateinit var sysBarAttrsListener: SystemBarAttributesListener @@ -57,7 +54,6 @@ class SystemBarAttributesListenerTest : SysuiTestCase() { sysBarAttrsListener = SystemBarAttributesListener( centralSurfaces, - featureFlags, letterboxAppearanceCalculator, statusBarStateController, lightBarController, @@ -74,18 +70,14 @@ class SystemBarAttributesListenerTest : SysuiTestCase() { } @Test - fun onSysBarAttrsChanged_flagTrue_forwardsLetterboxAppearanceToCentralSurfaces() { - whenever(featureFlags.isEnabled(Flags.STATUS_BAR_LETTERBOX_APPEARANCE)).thenReturn(true) - + fun onSysBarAttrsChanged_forwardsLetterboxAppearanceToCentralSurfaces() { changeSysBarAttrs(TEST_APPEARANCE, TEST_LETTERBOX_DETAILS) verify(centralSurfaces).setAppearance(TEST_LETTERBOX_APPEARANCE.appearance) } @Test - fun onSysBarAttrsChanged_flagTrue_noLetterbox_forwardsOriginalAppearanceToCtrlSrfcs() { - whenever(featureFlags.isEnabled(Flags.STATUS_BAR_LETTERBOX_APPEARANCE)).thenReturn(true) - + fun onSysBarAttrsChanged_noLetterbox_forwardsOriginalAppearanceToCtrlSrfcs() { changeSysBarAttrs(TEST_APPEARANCE, arrayOf<LetterboxDetails>()) verify(centralSurfaces).setAppearance(TEST_APPEARANCE) @@ -100,9 +92,7 @@ class SystemBarAttributesListenerTest : SysuiTestCase() { } @Test - fun onSysBarAttrsChanged_flagTrue_forwardsLetterboxAppearanceToStatusBarStateCtrl() { - whenever(featureFlags.isEnabled(Flags.STATUS_BAR_LETTERBOX_APPEARANCE)).thenReturn(true) - + fun onSysBarAttrsChanged_forwardsLetterboxAppearanceToStatusBarStateCtrl() { changeSysBarAttrs(TEST_APPEARANCE, TEST_LETTERBOX_DETAILS) verify(statusBarStateController) @@ -120,9 +110,7 @@ class SystemBarAttributesListenerTest : SysuiTestCase() { } @Test - fun onSysBarAttrsChanged_flagTrue_forwardsLetterboxAppearanceToLightBarController() { - whenever(featureFlags.isEnabled(Flags.STATUS_BAR_LETTERBOX_APPEARANCE)).thenReturn(true) - + fun onSysBarAttrsChanged_forwardsLetterboxAppearanceToLightBarController() { changeSysBarAttrs(TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, TEST_LETTERBOX_DETAILS) verify(lightBarController) @@ -135,7 +123,6 @@ class SystemBarAttributesListenerTest : SysuiTestCase() { @Test fun onStatusBarBoundsChanged_forwardsLetterboxAppearanceToStatusBarStateController() { - whenever(featureFlags.isEnabled(Flags.STATUS_BAR_LETTERBOX_APPEARANCE)).thenReturn(true) changeSysBarAttrs(TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, TEST_LETTERBOX_DETAILS) reset(centralSurfaces, lightBarController, statusBarStateController) @@ -148,7 +135,6 @@ class SystemBarAttributesListenerTest : SysuiTestCase() { @Test fun onStatusBarBoundsChanged_forwardsLetterboxAppearanceToLightBarController() { - whenever(featureFlags.isEnabled(Flags.STATUS_BAR_LETTERBOX_APPEARANCE)).thenReturn(true) changeSysBarAttrs(TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, TEST_LETTERBOX_DETAILS) reset(centralSurfaces, lightBarController, statusBarStateController) @@ -164,7 +150,6 @@ class SystemBarAttributesListenerTest : SysuiTestCase() { @Test fun onStatusBarBoundsChanged_forwardsLetterboxAppearanceToCentralSurfaces() { - whenever(featureFlags.isEnabled(Flags.STATUS_BAR_LETTERBOX_APPEARANCE)).thenReturn(true) changeSysBarAttrs(TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, TEST_LETTERBOX_DETAILS) reset(centralSurfaces, lightBarController, statusBarStateController) @@ -175,7 +160,6 @@ class SystemBarAttributesListenerTest : SysuiTestCase() { @Test fun onStatusBarBoundsChanged_previousCallEmptyLetterbox_doesNothing() { - whenever(featureFlags.isEnabled(Flags.STATUS_BAR_LETTERBOX_APPEARANCE)).thenReturn(true) changeSysBarAttrs(TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, arrayOf()) reset(centralSurfaces, lightBarController, statusBarStateController) @@ -184,17 +168,6 @@ class SystemBarAttributesListenerTest : SysuiTestCase() { verifyZeroInteractions(centralSurfaces, lightBarController, statusBarStateController) } - @Test - fun onStatusBarBoundsChanged_flagFalse_doesNothing() { - whenever(featureFlags.isEnabled(Flags.STATUS_BAR_LETTERBOX_APPEARANCE)).thenReturn(false) - changeSysBarAttrs(TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, TEST_LETTERBOX_DETAILS) - reset(centralSurfaces, lightBarController, statusBarStateController) - - sysBarAttrsListener.onStatusBarBoundsChanged() - - verifyZeroInteractions(centralSurfaces, lightBarController, statusBarStateController) - } - private fun changeSysBarAttrs(@Appearance appearance: Int) { changeSysBarAttrs(appearance, arrayOf<LetterboxDetails>()) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt index 6ff7b7ccd5e3..de1fec85360b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt @@ -24,7 +24,14 @@ class FakeMobileConnectionRepository : MobileConnectionRepository { private val _subscriptionsModelFlow = MutableStateFlow(MobileSubscriptionModel()) override val subscriptionModelFlow: Flow<MobileSubscriptionModel> = _subscriptionsModelFlow + private val _dataEnabled = MutableStateFlow(true) + override val dataEnabled = _dataEnabled + fun setMobileSubscriptionModel(model: MobileSubscriptionModel) { _subscriptionsModelFlow.value = model } + + fun setDataEnabled(enabled: Boolean) { + _dataEnabled.value = enabled + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt index c88d468f1755..813e750684a0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt @@ -50,7 +50,7 @@ class FakeMobileConnectionsRepository : MobileConnectionsRepository { _activeMobileDataSubscriptionId.value = subId } - fun setMobileConnectionRepositoryForId(subId: Int, repo: MobileConnectionRepository) { - subIdRepos[subId] = repo + fun setMobileConnectionRepositoryMap(connections: Map<Int, MobileConnectionRepository>) { + connections.forEach { entry -> subIdRepos[entry.key] = entry.value } } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt index 775e6dbb5e19..093936444789 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt @@ -20,16 +20,20 @@ import android.telephony.CellSignalStrengthCdma import android.telephony.ServiceState import android.telephony.SignalStrength import android.telephony.SubscriptionInfo -import android.telephony.SubscriptionManager import android.telephony.TelephonyCallback import android.telephony.TelephonyCallback.ServiceStateListener import android.telephony.TelephonyDisplayInfo import android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_LTE_CA import android.telephony.TelephonyManager +import android.telephony.TelephonyManager.DATA_CONNECTED +import android.telephony.TelephonyManager.DATA_CONNECTING +import android.telephony.TelephonyManager.DATA_DISCONNECTED +import android.telephony.TelephonyManager.DATA_DISCONNECTING import android.telephony.TelephonyManager.NETWORK_TYPE_LTE import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState import com.android.systemui.statusbar.pipeline.mobile.data.model.DefaultNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetworkType @@ -59,7 +63,6 @@ import org.mockito.MockitoAnnotations class MobileConnectionRepositoryTest : SysuiTestCase() { private lateinit var underTest: MobileConnectionRepositoryImpl - @Mock private lateinit var subscriptionManager: SubscriptionManager @Mock private lateinit var telephonyManager: TelephonyManager @Mock private lateinit var logger: ConnectivityPipelineLogger @@ -148,16 +151,61 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { } @Test - fun testFlowForSubId_dataConnectionState() = + fun testFlowForSubId_dataConnectionState_connected() = runBlocking(IMMEDIATE) { var latest: MobileSubscriptionModel? = null val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this) val callback = getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>() - callback.onDataConnectionStateChanged(100, 200 /* unused */) + callback.onDataConnectionStateChanged(DATA_CONNECTED, 200 /* unused */) - assertThat(latest?.dataConnectionState).isEqualTo(100) + assertThat(latest?.dataConnectionState).isEqualTo(DataConnectionState.Connected) + + job.cancel() + } + + @Test + fun testFlowForSubId_dataConnectionState_connecting() = + runBlocking(IMMEDIATE) { + var latest: MobileSubscriptionModel? = null + val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this) + + val callback = + getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>() + callback.onDataConnectionStateChanged(DATA_CONNECTING, 200 /* unused */) + + assertThat(latest?.dataConnectionState).isEqualTo(DataConnectionState.Connecting) + + job.cancel() + } + + @Test + fun testFlowForSubId_dataConnectionState_disconnected() = + runBlocking(IMMEDIATE) { + var latest: MobileSubscriptionModel? = null + val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this) + + val callback = + getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>() + callback.onDataConnectionStateChanged(DATA_DISCONNECTED, 200 /* unused */) + + assertThat(latest?.dataConnectionState).isEqualTo(DataConnectionState.Disconnected) + + job.cancel() + } + + @Test + fun testFlowForSubId_dataConnectionState_disconnecting() = + runBlocking(IMMEDIATE) { + var latest: MobileSubscriptionModel? = null + val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this) + + val callback = + getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>() + callback.onDataConnectionStateChanged(DATA_DISCONNECTING, 200 /* unused */) + + assertThat(latest?.dataConnectionState).isEqualTo(DataConnectionState.Disconnecting) job.cancel() } @@ -241,6 +289,32 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { job.cancel() } + @Test + fun dataEnabled_isEnabled() = + runBlocking(IMMEDIATE) { + whenever(telephonyManager.isDataConnectionAllowed).thenReturn(true) + + var latest: Boolean? = null + val job = underTest.dataEnabled.onEach { latest = it }.launchIn(this) + + assertThat(latest).isTrue() + + job.cancel() + } + + @Test + fun dataEnabled_isDisabled() = + runBlocking(IMMEDIATE) { + whenever(telephonyManager.isDataConnectionAllowed).thenReturn(false) + + var latest: Boolean? = null + val job = underTest.dataEnabled.onEach { latest = it }.launchIn(this) + + assertThat(latest).isFalse() + + job.cancel() + } + private fun getTelephonyCallbacks(): List<TelephonyCallback> { val callbackCaptor = argumentCaptor<TelephonyCallback>() Mockito.verify(telephonyManager).registerTelephonyCallback(any(), callbackCaptor.capture()) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt index cd4dbebcc35c..5611c448c550 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt @@ -22,19 +22,22 @@ import com.android.settingslib.mobile.TelephonyIcons import kotlinx.coroutines.flow.MutableStateFlow class FakeMobileIconInteractor : MobileIconInteractor { - private val _iconGroup = MutableStateFlow<SignalIcon.MobileIconGroup>(TelephonyIcons.UNKNOWN) + private val _iconGroup = MutableStateFlow<SignalIcon.MobileIconGroup>(TelephonyIcons.THREE_G) override val networkTypeIconGroup = _iconGroup - private val _isEmergencyOnly = MutableStateFlow<Boolean>(false) + private val _isEmergencyOnly = MutableStateFlow(false) override val isEmergencyOnly = _isEmergencyOnly - private val _level = MutableStateFlow<Int>(CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN) + private val _isDataEnabled = MutableStateFlow(true) + override val isDataEnabled = _isDataEnabled + + private val _level = MutableStateFlow(CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN) override val level = _level - private val _numberOfLevels = MutableStateFlow<Int>(4) + private val _numberOfLevels = MutableStateFlow(4) override val numberOfLevels = _numberOfLevels - private val _cutOut = MutableStateFlow<Boolean>(false) + private val _cutOut = MutableStateFlow(false) override val cutOut = _cutOut fun setIconGroup(group: SignalIcon.MobileIconGroup) { @@ -45,6 +48,10 @@ class FakeMobileIconInteractor : MobileIconInteractor { _isEmergencyOnly.value = emergency } + fun setIsDataEnabled(enabled: Boolean) { + _isDataEnabled.value = enabled + } + fun setLevel(level: Int) { _level.value = level } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt index b01efd18971f..877ce0e6b351 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.pipeline.mobile.domain.interactor import android.telephony.SubscriptionInfo import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeUserSetupRepository import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy @@ -41,7 +42,7 @@ import org.mockito.MockitoAnnotations class MobileIconsInteractorTest : SysuiTestCase() { private lateinit var underTest: MobileIconsInteractor private val userSetupRepository = FakeUserSetupRepository() - private val subscriptionsRepository = FakeMobileConnectionsRepository() + private val connectionsRepository = FakeMobileConnectionsRepository() private val mobileMappingsProxy = FakeMobileMappingsProxy() private val scope = CoroutineScope(IMMEDIATE) @@ -50,9 +51,20 @@ class MobileIconsInteractorTest : SysuiTestCase() { @Before fun setUp() { MockitoAnnotations.initMocks(this) + + connectionsRepository.setMobileConnectionRepositoryMap( + mapOf( + SUB_1_ID to CONNECTION_1, + SUB_2_ID to CONNECTION_2, + SUB_3_ID to CONNECTION_3, + SUB_4_ID to CONNECTION_4, + ) + ) + connectionsRepository.setActiveMobileDataSubscriptionId(SUB_1_ID) + underTest = MobileIconsInteractorImpl( - subscriptionsRepository, + connectionsRepository, carrierConfigTracker, mobileMappingsProxy, userSetupRepository, @@ -76,7 +88,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { @Test fun filteredSubscriptions_nonOpportunistic_updatesWithMultipleSubs() = runBlocking(IMMEDIATE) { - subscriptionsRepository.setSubscriptions(listOf(SUB_1, SUB_2)) + connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_2)) var latest: List<SubscriptionInfo>? = null val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this) @@ -89,8 +101,8 @@ class MobileIconsInteractorTest : SysuiTestCase() { @Test fun filteredSubscriptions_bothOpportunistic_configFalse_showsActive_3() = runBlocking(IMMEDIATE) { - subscriptionsRepository.setSubscriptions(listOf(SUB_3_OPP, SUB_4_OPP)) - subscriptionsRepository.setActiveMobileDataSubscriptionId(SUB_3_ID) + connectionsRepository.setSubscriptions(listOf(SUB_3_OPP, SUB_4_OPP)) + connectionsRepository.setActiveMobileDataSubscriptionId(SUB_3_ID) whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault) .thenReturn(false) @@ -106,8 +118,8 @@ class MobileIconsInteractorTest : SysuiTestCase() { @Test fun filteredSubscriptions_bothOpportunistic_configFalse_showsActive_4() = runBlocking(IMMEDIATE) { - subscriptionsRepository.setSubscriptions(listOf(SUB_3_OPP, SUB_4_OPP)) - subscriptionsRepository.setActiveMobileDataSubscriptionId(SUB_4_ID) + connectionsRepository.setSubscriptions(listOf(SUB_3_OPP, SUB_4_OPP)) + connectionsRepository.setActiveMobileDataSubscriptionId(SUB_4_ID) whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault) .thenReturn(false) @@ -123,8 +135,8 @@ class MobileIconsInteractorTest : SysuiTestCase() { @Test fun filteredSubscriptions_oneOpportunistic_configTrue_showsPrimary_active_1() = runBlocking(IMMEDIATE) { - subscriptionsRepository.setSubscriptions(listOf(SUB_1, SUB_3_OPP)) - subscriptionsRepository.setActiveMobileDataSubscriptionId(SUB_1_ID) + connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_3_OPP)) + connectionsRepository.setActiveMobileDataSubscriptionId(SUB_1_ID) whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault) .thenReturn(true) @@ -141,8 +153,8 @@ class MobileIconsInteractorTest : SysuiTestCase() { @Test fun filteredSubscriptions_oneOpportunistic_configTrue_showsPrimary_nonActive_1() = runBlocking(IMMEDIATE) { - subscriptionsRepository.setSubscriptions(listOf(SUB_1, SUB_3_OPP)) - subscriptionsRepository.setActiveMobileDataSubscriptionId(SUB_3_ID) + connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_3_OPP)) + connectionsRepository.setActiveMobileDataSubscriptionId(SUB_3_ID) whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault) .thenReturn(true) @@ -162,10 +174,12 @@ class MobileIconsInteractorTest : SysuiTestCase() { private const val SUB_1_ID = 1 private val SUB_1 = mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) } + private val CONNECTION_1 = FakeMobileConnectionRepository() private const val SUB_2_ID = 2 private val SUB_2 = mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_2_ID) } + private val CONNECTION_2 = FakeMobileConnectionRepository() private const val SUB_3_ID = 3 private val SUB_3_OPP = @@ -173,6 +187,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { whenever(it.subscriptionId).thenReturn(SUB_3_ID) whenever(it.isOpportunistic).thenReturn(true) } + private val CONNECTION_3 = FakeMobileConnectionRepository() private const val SUB_4_ID = 4 private val SUB_4_OPP = @@ -180,5 +195,6 @@ class MobileIconsInteractorTest : SysuiTestCase() { whenever(it.subscriptionId).thenReturn(SUB_4_ID) whenever(it.isOpportunistic).thenReturn(true) } + private val CONNECTION_4 = FakeMobileConnectionRepository() } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt index b374abbd5082..ce0f33f400ab 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt @@ -18,8 +18,10 @@ package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel import androidx.test.filters.SmallTest import com.android.settingslib.graph.SignalDrawable -import com.android.settingslib.mobile.TelephonyIcons +import com.android.settingslib.mobile.TelephonyIcons.THREE_G import com.android.systemui.SysuiTestCase +import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.common.shared.model.Icon import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconInteractor import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger import com.google.common.truth.Truth.assertThat @@ -27,6 +29,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.yield import org.junit.Before import org.junit.Test import org.mockito.Mock @@ -44,7 +47,7 @@ class MobileIconViewModelTest : SysuiTestCase() { interactor.apply { setLevel(1) setCutOut(false) - setIconGroup(TelephonyIcons.THREE_G) + setIconGroup(THREE_G) setIsEmergencyOnly(false) setNumberOfLevels(4) } @@ -62,6 +65,60 @@ class MobileIconViewModelTest : SysuiTestCase() { job.cancel() } + @Test + fun networkType_dataEnabled_groupIsRepresented() = + runBlocking(IMMEDIATE) { + val expected = + Icon.Resource( + THREE_G.dataType, + ContentDescription.Resource(THREE_G.dataContentDescription) + ) + interactor.setIconGroup(THREE_G) + + var latest: Icon? = null + val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this) + + assertThat(latest).isEqualTo(expected) + + job.cancel() + } + + @Test + fun networkType_nullWhenDisabled() = + runBlocking(IMMEDIATE) { + interactor.setIconGroup(THREE_G) + interactor.setIsDataEnabled(false) + var latest: Icon? = null + val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this) + + assertThat(latest).isNull() + + job.cancel() + } + + @Test + fun networkType_null_changeToDisabled() = + runBlocking(IMMEDIATE) { + val expected = + Icon.Resource( + THREE_G.dataType, + ContentDescription.Resource(THREE_G.dataContentDescription) + ) + interactor.setIconGroup(THREE_G) + interactor.setIsDataEnabled(true) + var latest: Icon? = null + val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this) + + assertThat(latest).isEqualTo(expected) + + interactor.setIsDataEnabled(false) + yield() + + assertThat(latest).isNull() + + job.cancel() + } + companion object { private val IMMEDIATE = Dispatchers.Main.immediate private const val SUB_1_ID = 1 diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt index b68eb88d46db..91b5c35d9661 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt @@ -41,6 +41,7 @@ import org.junit.Test import org.mockito.Mock import org.mockito.Mockito.never import org.mockito.Mockito.reset +import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.Mockito.`when` as whenever import org.mockito.MockitoAnnotations @@ -85,10 +86,29 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() { } @Test - fun displayView_viewAdded() { - underTest.displayView(getState()) + fun displayView_viewAddedWithCorrectTitle() { + underTest.displayView( + ViewInfo( + name = "name", + windowTitle = "Fake Window Title", + ) + ) - verify(windowManager).addView(any(), any()) + val windowParamsCaptor = argumentCaptor<WindowManager.LayoutParams>() + verify(windowManager).addView(any(), capture(windowParamsCaptor)) + assertThat(windowParamsCaptor.value!!.title).isEqualTo("Fake Window Title") + } + + @Test + fun displayView_logged() { + underTest.displayView( + ViewInfo( + name = "name", + windowTitle = "Fake Window Title", + ) + ) + + verify(logger).logViewAddition("Fake Window Title") } @Test @@ -110,7 +130,7 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() { } @Test - fun displayView_twice_viewNotAddedTwice() { + fun displayView_twiceWithSameWindowTitle_viewNotAddedTwice() { underTest.displayView(getState()) reset(windowManager) @@ -119,6 +139,32 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() { } @Test + fun displayView_twiceWithDifferentWindowTitles_oldViewRemovedNewViewAdded() { + underTest.displayView( + ViewInfo( + name = "name", + windowTitle = "First Fake Window Title", + ) + ) + + underTest.displayView( + ViewInfo( + name = "name", + windowTitle = "Second Fake Window Title", + ) + ) + + val viewCaptor = argumentCaptor<View>() + val windowParamsCaptor = argumentCaptor<WindowManager.LayoutParams>() + + verify(windowManager, times(2)).addView(capture(viewCaptor), capture(windowParamsCaptor)) + + assertThat(windowParamsCaptor.allValues[0].title).isEqualTo("First Fake Window Title") + assertThat(windowParamsCaptor.allValues[1].title).isEqualTo("Second Fake Window Title") + verify(windowManager).removeView(viewCaptor.allValues[0]) + } + + @Test fun displayView_viewDoesNotDisappearsBeforeTimeout() { val state = getState() underTest.displayView(state) @@ -197,7 +243,7 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() { underTest.removeView(reason) verify(windowManager).removeView(any()) - verify(logger).logChipRemoval(reason) + verify(logger).logViewRemoval(reason) } @Test @@ -232,8 +278,6 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() { configurationController, powerManager, R.layout.chipbar, - "Window Title", - "WAKE_REASON", ) { var mostRecentViewInfo: ViewInfo? = null @@ -250,9 +294,12 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() { } } - inner class ViewInfo(val name: String) : TemporaryViewInfo { - override fun getTimeoutMs() = 1L - } + inner class ViewInfo( + val name: String, + override val windowTitle: String = "Window Title", + override val wakeReason: String = "WAKE_REASON", + override val timeoutMs: Int = 1 + ) : TemporaryViewInfo() } private const val TIMEOUT_MS = 10000L diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewLoggerTest.kt index 13e9f608158e..d155050ce932 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewLoggerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewLoggerTest.kt @@ -43,20 +43,21 @@ class TemporaryViewLoggerTest : SysuiTestCase() { } @Test - fun logChipAddition_bufferHasLog() { - logger.logChipAddition() + fun logViewAddition_bufferHasLog() { + logger.logViewAddition("Test Window Title") val stringWriter = StringWriter() buffer.dump(PrintWriter(stringWriter), tailLength = 0) val actualString = stringWriter.toString() assertThat(actualString).contains(TAG) + assertThat(actualString).contains("Test Window Title") } @Test - fun logChipRemoval_bufferHasTagAndReason() { + fun logViewRemoval_bufferHasTagAndReason() { val reason = "test reason" - logger.logChipRemoval(reason) + logger.logViewRemoval(reason) val stringWriter = StringWriter() buffer.dump(PrintWriter(stringWriter), tailLength = 0) diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt index 9fbf159ec348..f64397325867 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt @@ -35,12 +35,12 @@ import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.shared.model.Text -import com.android.systemui.media.taptotransfer.common.MediaTttLogger import com.android.systemui.plugins.FalsingManager import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.eq import com.android.systemui.util.time.FakeSystemClock import com.android.systemui.util.view.ViewUtil import com.google.common.truth.Truth.assertThat @@ -60,7 +60,7 @@ import org.mockito.MockitoAnnotations class ChipbarCoordinatorTest : SysuiTestCase() { private lateinit var underTest: FakeChipbarCoordinator - @Mock private lateinit var logger: MediaTttLogger + @Mock private lateinit var logger: ChipbarLogger @Mock private lateinit var accessibilityManager: AccessibilityManager @Mock private lateinit var configurationController: ConfigurationController @Mock private lateinit var powerManager: PowerManager @@ -105,7 +105,7 @@ class ChipbarCoordinatorTest : SysuiTestCase() { val drawable = context.getDrawable(R.drawable.ic_celebration)!! underTest.displayView( - ChipbarInfo( + createChipbarInfo( Icon.Loaded(drawable, contentDescription = ContentDescription.Loaded("loadedCD")), Text.Loaded("text"), endItem = null, @@ -121,7 +121,7 @@ class ChipbarCoordinatorTest : SysuiTestCase() { fun displayView_resourceIcon_correctlyRendered() { val contentDescription = ContentDescription.Resource(R.string.controls_error_timeout) underTest.displayView( - ChipbarInfo( + createChipbarInfo( Icon.Resource(R.drawable.ic_cake, contentDescription), Text.Loaded("text"), endItem = null, @@ -136,7 +136,7 @@ class ChipbarCoordinatorTest : SysuiTestCase() { @Test fun displayView_loadedText_correctlyRendered() { underTest.displayView( - ChipbarInfo( + createChipbarInfo( Icon.Resource(R.id.check_box, null), Text.Loaded("display view text here"), endItem = null, @@ -149,7 +149,7 @@ class ChipbarCoordinatorTest : SysuiTestCase() { @Test fun displayView_resourceText_correctlyRendered() { underTest.displayView( - ChipbarInfo( + createChipbarInfo( Icon.Resource(R.id.check_box, null), Text.Resource(R.string.screenrecord_start_error), endItem = null, @@ -163,7 +163,7 @@ class ChipbarCoordinatorTest : SysuiTestCase() { @Test fun displayView_endItemNull_correctlyRendered() { underTest.displayView( - ChipbarInfo( + createChipbarInfo( Icon.Resource(R.id.check_box, null), Text.Loaded("text"), endItem = null, @@ -179,7 +179,7 @@ class ChipbarCoordinatorTest : SysuiTestCase() { @Test fun displayView_endItemLoading_correctlyRendered() { underTest.displayView( - ChipbarInfo( + createChipbarInfo( Icon.Resource(R.id.check_box, null), Text.Loaded("text"), endItem = ChipbarEndItem.Loading, @@ -195,7 +195,7 @@ class ChipbarCoordinatorTest : SysuiTestCase() { @Test fun displayView_endItemError_correctlyRendered() { underTest.displayView( - ChipbarInfo( + createChipbarInfo( Icon.Resource(R.id.check_box, null), Text.Loaded("text"), endItem = ChipbarEndItem.Error, @@ -211,7 +211,7 @@ class ChipbarCoordinatorTest : SysuiTestCase() { @Test fun displayView_endItemButton_correctlyRendered() { underTest.displayView( - ChipbarInfo( + createChipbarInfo( Icon.Resource(R.id.check_box, null), Text.Loaded("text"), endItem = @@ -237,7 +237,7 @@ class ChipbarCoordinatorTest : SysuiTestCase() { val buttonClickListener = View.OnClickListener { isClicked = true } underTest.displayView( - ChipbarInfo( + createChipbarInfo( Icon.Resource(R.id.check_box, null), Text.Loaded("text"), endItem = @@ -260,7 +260,7 @@ class ChipbarCoordinatorTest : SysuiTestCase() { val buttonClickListener = View.OnClickListener { isClicked = true } underTest.displayView( - ChipbarInfo( + createChipbarInfo( Icon.Resource(R.id.check_box, null), Text.Loaded("text"), endItem = @@ -279,7 +279,7 @@ class ChipbarCoordinatorTest : SysuiTestCase() { @Test fun displayView_vibrationEffect_doubleClickEffect() { underTest.displayView( - ChipbarInfo( + createChipbarInfo( Icon.Resource(R.id.check_box, null), Text.Loaded("text"), endItem = null, @@ -296,7 +296,7 @@ class ChipbarCoordinatorTest : SysuiTestCase() { val drawable = context.getDrawable(R.drawable.ic_celebration)!! underTest.displayView( - ChipbarInfo( + createChipbarInfo( Icon.Loaded(drawable, contentDescription = ContentDescription.Loaded("loadedCD")), Text.Loaded("title text"), endItem = ChipbarEndItem.Loading, @@ -314,7 +314,7 @@ class ChipbarCoordinatorTest : SysuiTestCase() { // WHEN the view is updated val newDrawable = context.getDrawable(R.drawable.ic_cake)!! underTest.updateView( - ChipbarInfo( + createChipbarInfo( Icon.Loaded(newDrawable, ContentDescription.Loaded("new CD")), Text.Loaded("new title text"), endItem = ChipbarEndItem.Error, @@ -331,6 +331,47 @@ class ChipbarCoordinatorTest : SysuiTestCase() { assertThat(chipbarView.getEndButton().visibility).isEqualTo(View.GONE) } + @Test + fun viewUpdates_logged() { + val drawable = context.getDrawable(R.drawable.ic_celebration)!! + underTest.displayView( + createChipbarInfo( + Icon.Loaded(drawable, contentDescription = ContentDescription.Loaded("loadedCD")), + Text.Loaded("title text"), + endItem = ChipbarEndItem.Loading, + ) + ) + + verify(logger).logViewUpdate(eq(WINDOW_TITLE), eq("title text"), any()) + + underTest.displayView( + createChipbarInfo( + Icon.Loaded(drawable, ContentDescription.Loaded("new CD")), + Text.Loaded("new title text"), + endItem = ChipbarEndItem.Error, + ) + ) + + verify(logger).logViewUpdate(eq(WINDOW_TITLE), eq("new title text"), any()) + } + + private fun createChipbarInfo( + startIcon: Icon, + text: Text, + endItem: ChipbarEndItem?, + vibrationEffect: VibrationEffect? = null, + ): ChipbarInfo { + return ChipbarInfo( + startIcon, + text, + endItem, + vibrationEffect, + windowTitle = WINDOW_TITLE, + wakeReason = WAKE_REASON, + timeoutMs = TIMEOUT, + ) + } + private fun ViewGroup.getStartIconView() = this.requireViewById<ImageView>(R.id.start_icon) private fun ViewGroup.getChipText(): String = @@ -350,3 +391,5 @@ class ChipbarCoordinatorTest : SysuiTestCase() { } private const val TIMEOUT = 10000 +private const val WINDOW_TITLE = "Test Chipbar Window Title" +private const val WAKE_REASON = "TEST_CHIPBAR_WAKE_REASON" diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt index 17d402319246..574f70e7fddc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt @@ -22,8 +22,6 @@ import android.view.ViewGroup import android.view.WindowManager import android.view.accessibility.AccessibilityManager import com.android.systemui.classifier.FalsingCollector -import com.android.systemui.media.taptotransfer.common.MediaTttLogger -import com.android.systemui.media.taptotransfer.receiver.MediaTttReceiverLogger import com.android.systemui.plugins.FalsingManager import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.policy.ConfigurationController @@ -33,7 +31,7 @@ import com.android.systemui.util.view.ViewUtil /** A fake implementation of [ChipbarCoordinator] for testing. */ class FakeChipbarCoordinator( context: Context, - @MediaTttReceiverLogger logger: MediaTttLogger, + logger: ChipbarLogger, windowManager: WindowManager, mainExecutor: DelayableExecutor, accessibilityManager: AccessibilityManager, diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/DeviceFoldStateProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/DeviceFoldStateProviderTest.kt index e18dd3a3c846..7d5f06c890c2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/DeviceFoldStateProviderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/DeviceFoldStateProviderTest.kt @@ -140,6 +140,40 @@ class DeviceFoldStateProviderTest : SysuiTestCase() { } @Test + fun testOnUnfold_hingeAngleDecreasesBeforeInnerScreenAvailable_emitsOnlyStartAndInnerScreenAvailableEvents() { + setFoldState(folded = true) + foldUpdates.clear() + + setFoldState(folded = false) + screenOnStatusProvider.notifyScreenTurningOn() + sendHingeAngleEvent(10) + sendHingeAngleEvent(20) + sendHingeAngleEvent(10) + screenOnStatusProvider.notifyScreenTurnedOn() + + assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_OPENING, + FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE) + } + + @Test + fun testOnUnfold_hingeAngleDecreasesAfterInnerScreenAvailable_emitsStartInnerScreenAvailableAndStartClosingEvents() { + setFoldState(folded = true) + foldUpdates.clear() + + setFoldState(folded = false) + screenOnStatusProvider.notifyScreenTurningOn() + sendHingeAngleEvent(10) + sendHingeAngleEvent(20) + screenOnStatusProvider.notifyScreenTurnedOn() + sendHingeAngleEvent(30) + sendHingeAngleEvent(40) + sendHingeAngleEvent(10) + + assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_OPENING, + FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE, FOLD_UPDATE_START_CLOSING) + } + + @Test fun testOnFolded_stopsHingeAngleProvider() { setFoldState(folded = true) @@ -237,7 +271,7 @@ class DeviceFoldStateProviderTest : SysuiTestCase() { } @Test - fun startClosingEvent_afterTimeout_abortEmitted() { + fun startClosingEvent_afterTimeout_finishHalfOpenEventEmitted() { sendHingeAngleEvent(90) sendHingeAngleEvent(80) @@ -269,7 +303,7 @@ class DeviceFoldStateProviderTest : SysuiTestCase() { } @Test - fun startClosingEvent_timeoutAfterTimeoutRescheduled_abortEmitted() { + fun startClosingEvent_timeoutAfterTimeoutRescheduled_finishHalfOpenStateEmitted() { sendHingeAngleEvent(180) sendHingeAngleEvent(90) diff --git a/packages/SystemUI/tests/src/com/android/systemui/wallpapers/canvas/WallpaperColorExtractorTest.java b/packages/SystemUI/tests/src/com/android/systemui/wallpapers/canvas/WallpaperLocalColorExtractorTest.java index 76bff1d72141..7e8ffeb7f9e1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wallpapers/canvas/WallpaperColorExtractorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wallpapers/canvas/WallpaperLocalColorExtractorTest.java @@ -54,7 +54,7 @@ import java.util.concurrent.Executor; @SmallTest @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper -public class WallpaperColorExtractorTest extends SysuiTestCase { +public class WallpaperLocalColorExtractorTest extends SysuiTestCase { private static final int LOW_BMP_WIDTH = 128; private static final int LOW_BMP_HEIGHT = 128; private static final int HIGH_BMP_WIDTH = 3000; @@ -105,11 +105,11 @@ public class WallpaperColorExtractorTest extends SysuiTestCase { return bitmap; } - private WallpaperColorExtractor getSpyWallpaperColorExtractor() { + private WallpaperLocalColorExtractor getSpyWallpaperLocalColorExtractor() { - WallpaperColorExtractor wallpaperColorExtractor = new WallpaperColorExtractor( + WallpaperLocalColorExtractor colorExtractor = new WallpaperLocalColorExtractor( mBackgroundExecutor, - new WallpaperColorExtractor.WallpaperColorExtractorCallback() { + new WallpaperLocalColorExtractor.WallpaperLocalColorExtractorCallback() { @Override public void onColorsProcessed(List<RectF> regions, List<WallpaperColors> colors) { @@ -132,25 +132,25 @@ public class WallpaperColorExtractorTest extends SysuiTestCase { mDeactivatedCount++; } }); - WallpaperColorExtractor spyWallpaperColorExtractor = spy(wallpaperColorExtractor); + WallpaperLocalColorExtractor spyColorExtractor = spy(colorExtractor); doAnswer(invocation -> { mMiniBitmapWidth = invocation.getArgument(1); mMiniBitmapHeight = invocation.getArgument(2); return getMockBitmap(mMiniBitmapWidth, mMiniBitmapHeight); - }).when(spyWallpaperColorExtractor).createMiniBitmap(any(Bitmap.class), anyInt(), anyInt()); + }).when(spyColorExtractor).createMiniBitmap(any(Bitmap.class), anyInt(), anyInt()); doAnswer(invocation -> getMockBitmap( invocation.getArgument(1), invocation.getArgument(2))) - .when(spyWallpaperColorExtractor) + .when(spyColorExtractor) .createMiniBitmap(any(Bitmap.class), anyInt(), anyInt()); doReturn(new WallpaperColors(Color.valueOf(0), Color.valueOf(0), Color.valueOf(0))) - .when(spyWallpaperColorExtractor).getLocalWallpaperColors(any(Rect.class)); + .when(spyColorExtractor).getLocalWallpaperColors(any(Rect.class)); - return spyWallpaperColorExtractor; + return spyColorExtractor; } private RectF randomArea() { @@ -180,18 +180,18 @@ public class WallpaperColorExtractorTest extends SysuiTestCase { */ @Test public void testMiniBitmapCreation() { - WallpaperColorExtractor spyWallpaperColorExtractor = getSpyWallpaperColorExtractor(); + WallpaperLocalColorExtractor spyColorExtractor = getSpyWallpaperLocalColorExtractor(); int nSimulations = 10; for (int i = 0; i < nSimulations; i++) { resetCounters(); int width = randomBetween(LOW_BMP_WIDTH, HIGH_BMP_WIDTH); int height = randomBetween(LOW_BMP_HEIGHT, HIGH_BMP_HEIGHT); Bitmap bitmap = getMockBitmap(width, height); - spyWallpaperColorExtractor.onBitmapChanged(bitmap); + spyColorExtractor.onBitmapChanged(bitmap); assertThat(mMiniBitmapUpdatedCount).isEqualTo(1); assertThat(Math.min(mMiniBitmapWidth, mMiniBitmapHeight)) - .isAtMost(WallpaperColorExtractor.SMALL_SIDE); + .isAtMost(WallpaperLocalColorExtractor.SMALL_SIDE); } } @@ -201,18 +201,18 @@ public class WallpaperColorExtractorTest extends SysuiTestCase { */ @Test public void testSmallMiniBitmapCreation() { - WallpaperColorExtractor spyWallpaperColorExtractor = getSpyWallpaperColorExtractor(); + WallpaperLocalColorExtractor spyColorExtractor = getSpyWallpaperLocalColorExtractor(); int nSimulations = 10; for (int i = 0; i < nSimulations; i++) { resetCounters(); int width = randomBetween(VERY_LOW_BMP_WIDTH, LOW_BMP_WIDTH); int height = randomBetween(VERY_LOW_BMP_HEIGHT, LOW_BMP_HEIGHT); Bitmap bitmap = getMockBitmap(width, height); - spyWallpaperColorExtractor.onBitmapChanged(bitmap); + spyColorExtractor.onBitmapChanged(bitmap); assertThat(mMiniBitmapUpdatedCount).isEqualTo(1); assertThat(Math.max(mMiniBitmapWidth, mMiniBitmapHeight)) - .isAtMost(WallpaperColorExtractor.SMALL_SIDE); + .isAtMost(WallpaperLocalColorExtractor.SMALL_SIDE); } } @@ -228,15 +228,15 @@ public class WallpaperColorExtractorTest extends SysuiTestCase { int nSimulations = 10; for (int i = 0; i < nSimulations; i++) { resetCounters(); - WallpaperColorExtractor spyWallpaperColorExtractor = getSpyWallpaperColorExtractor(); + WallpaperLocalColorExtractor spyColorExtractor = getSpyWallpaperLocalColorExtractor(); List<RectF> regions = listOfRandomAreas(MIN_AREAS, MAX_AREAS); int nPages = randomBetween(PAGES_LOW, PAGES_HIGH); List<Runnable> tasks = Arrays.asList( - () -> spyWallpaperColorExtractor.onPageChanged(nPages), - () -> spyWallpaperColorExtractor.onBitmapChanged(bitmap), - () -> spyWallpaperColorExtractor.setDisplayDimensions( + () -> spyColorExtractor.onPageChanged(nPages), + () -> spyColorExtractor.onBitmapChanged(bitmap), + () -> spyColorExtractor.setDisplayDimensions( DISPLAY_WIDTH, DISPLAY_HEIGHT), - () -> spyWallpaperColorExtractor.addLocalColorsAreas( + () -> spyColorExtractor.addLocalColorsAreas( regions)); Collections.shuffle(tasks); tasks.forEach(Runnable::run); @@ -245,7 +245,7 @@ public class WallpaperColorExtractorTest extends SysuiTestCase { assertThat(mMiniBitmapUpdatedCount).isEqualTo(1); assertThat(mColorsProcessed).isEqualTo(regions.size()); - spyWallpaperColorExtractor.removeLocalColorAreas(regions); + spyColorExtractor.removeLocalColorAreas(regions); assertThat(mDeactivatedCount).isEqualTo(1); } } @@ -260,7 +260,7 @@ public class WallpaperColorExtractorTest extends SysuiTestCase { int nSimulations = 10; for (int i = 0; i < nSimulations; i++) { resetCounters(); - WallpaperColorExtractor spyWallpaperColorExtractor = getSpyWallpaperColorExtractor(); + WallpaperLocalColorExtractor spyColorExtractor = getSpyWallpaperLocalColorExtractor(); List<RectF> regions1 = listOfRandomAreas(MIN_AREAS / 2, MAX_AREAS / 2); List<RectF> regions2 = listOfRandomAreas(MIN_AREAS / 2, MAX_AREAS / 2); List<RectF> regions = new ArrayList<>(); @@ -268,20 +268,20 @@ public class WallpaperColorExtractorTest extends SysuiTestCase { regions.addAll(regions2); int nPages = randomBetween(PAGES_LOW, PAGES_HIGH); List<Runnable> tasks = Arrays.asList( - () -> spyWallpaperColorExtractor.onPageChanged(nPages), - () -> spyWallpaperColorExtractor.onBitmapChanged(bitmap), - () -> spyWallpaperColorExtractor.setDisplayDimensions( + () -> spyColorExtractor.onPageChanged(nPages), + () -> spyColorExtractor.onBitmapChanged(bitmap), + () -> spyColorExtractor.setDisplayDimensions( DISPLAY_WIDTH, DISPLAY_HEIGHT), - () -> spyWallpaperColorExtractor.removeLocalColorAreas(regions1)); + () -> spyColorExtractor.removeLocalColorAreas(regions1)); - spyWallpaperColorExtractor.addLocalColorsAreas(regions); + spyColorExtractor.addLocalColorsAreas(regions); assertThat(mActivatedCount).isEqualTo(1); Collections.shuffle(tasks); tasks.forEach(Runnable::run); assertThat(mMiniBitmapUpdatedCount).isEqualTo(1); assertThat(mDeactivatedCount).isEqualTo(0); - spyWallpaperColorExtractor.removeLocalColorAreas(regions2); + spyColorExtractor.removeLocalColorAreas(regions2); assertThat(mDeactivatedCount).isEqualTo(1); } } @@ -295,18 +295,18 @@ public class WallpaperColorExtractorTest extends SysuiTestCase { @Test public void testRecomputeColorExtraction() { Bitmap bitmap = getMockBitmap(HIGH_BMP_WIDTH, HIGH_BMP_HEIGHT); - WallpaperColorExtractor spyWallpaperColorExtractor = getSpyWallpaperColorExtractor(); + WallpaperLocalColorExtractor spyColorExtractor = getSpyWallpaperLocalColorExtractor(); List<RectF> regions1 = listOfRandomAreas(MIN_AREAS / 2, MAX_AREAS / 2); List<RectF> regions2 = listOfRandomAreas(MIN_AREAS / 2, MAX_AREAS / 2); List<RectF> regions = new ArrayList<>(); regions.addAll(regions1); regions.addAll(regions2); - spyWallpaperColorExtractor.addLocalColorsAreas(regions); + spyColorExtractor.addLocalColorsAreas(regions); assertThat(mActivatedCount).isEqualTo(1); int nPages = PAGES_LOW; - spyWallpaperColorExtractor.onBitmapChanged(bitmap); - spyWallpaperColorExtractor.onPageChanged(nPages); - spyWallpaperColorExtractor.setDisplayDimensions(DISPLAY_WIDTH, DISPLAY_HEIGHT); + spyColorExtractor.onBitmapChanged(bitmap); + spyColorExtractor.onPageChanged(nPages); + spyColorExtractor.setDisplayDimensions(DISPLAY_WIDTH, DISPLAY_HEIGHT); int nSimulations = 20; for (int i = 0; i < nSimulations; i++) { @@ -315,22 +315,22 @@ public class WallpaperColorExtractorTest extends SysuiTestCase { // verify that if we remove some regions, they are not recomputed after other changes if (i == nSimulations / 2) { regions.removeAll(regions2); - spyWallpaperColorExtractor.removeLocalColorAreas(regions2); + spyColorExtractor.removeLocalColorAreas(regions2); } if (Math.random() >= 0.5) { int nPagesNew = randomBetween(PAGES_LOW, PAGES_HIGH); if (nPagesNew == nPages) continue; nPages = nPagesNew; - spyWallpaperColorExtractor.onPageChanged(nPagesNew); + spyColorExtractor.onPageChanged(nPagesNew); } else { Bitmap newBitmap = getMockBitmap(HIGH_BMP_WIDTH, HIGH_BMP_HEIGHT); - spyWallpaperColorExtractor.onBitmapChanged(newBitmap); + spyColorExtractor.onBitmapChanged(newBitmap); assertThat(mMiniBitmapUpdatedCount).isEqualTo(1); } assertThat(mColorsProcessed).isEqualTo(regions.size()); } - spyWallpaperColorExtractor.removeLocalColorAreas(regions); + spyColorExtractor.removeLocalColorAreas(regions); assertThat(mDeactivatedCount).isEqualTo(1); } @@ -339,12 +339,12 @@ public class WallpaperColorExtractorTest extends SysuiTestCase { resetCounters(); Bitmap bitmap = getMockBitmap(HIGH_BMP_WIDTH, HIGH_BMP_HEIGHT); doNothing().when(bitmap).recycle(); - WallpaperColorExtractor spyWallpaperColorExtractor = getSpyWallpaperColorExtractor(); - spyWallpaperColorExtractor.onPageChanged(PAGES_LOW); - spyWallpaperColorExtractor.onBitmapChanged(bitmap); + WallpaperLocalColorExtractor spyColorExtractor = getSpyWallpaperLocalColorExtractor(); + spyColorExtractor.onPageChanged(PAGES_LOW); + spyColorExtractor.onBitmapChanged(bitmap); assertThat(mMiniBitmapUpdatedCount).isEqualTo(1); - spyWallpaperColorExtractor.cleanUp(); - spyWallpaperColorExtractor.addLocalColorsAreas(listOfRandomAreas(MIN_AREAS, MAX_AREAS)); + spyColorExtractor.cleanUp(); + spyColorExtractor.addLocalColorsAreas(listOfRandomAreas(MIN_AREAS, MAX_AREAS)); assertThat(mColorsProcessed).isEqualTo(0); } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java index 7af66f641837..7ae47b41d5ae 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java @@ -28,6 +28,7 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.keyguard.ScreenLifecycle; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.model.SysUiState; +import com.android.systemui.notetask.NoteTaskInitializer; import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.policy.ConfigurationController; @@ -36,7 +37,6 @@ import com.android.systemui.tracing.ProtoTracer; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.desktopmode.DesktopMode; import com.android.wm.shell.desktopmode.DesktopModeTaskRepository; -import com.android.wm.shell.floating.FloatingTasks; import com.android.wm.shell.onehanded.OneHanded; import com.android.wm.shell.onehanded.OneHandedEventCallback; import com.android.wm.shell.onehanded.OneHandedTransitionCallback; @@ -78,18 +78,31 @@ public class WMShellTest extends SysuiTestCase { @Mock ProtoTracer mProtoTracer; @Mock UserTracker mUserTracker; @Mock ShellExecutor mSysUiMainExecutor; - @Mock FloatingTasks mFloatingTasks; + @Mock NoteTaskInitializer mNoteTaskInitializer; @Mock DesktopMode mDesktopMode; @Before public void setUp() { MockitoAnnotations.initMocks(this); - mWMShell = new WMShell(mContext, mShellInterface, Optional.of(mPip), - Optional.of(mSplitScreen), Optional.of(mOneHanded), Optional.of(mFloatingTasks), + mWMShell = new WMShell( + mContext, + mShellInterface, + Optional.of(mPip), + Optional.of(mSplitScreen), + Optional.of(mOneHanded), Optional.of(mDesktopMode), - mCommandQueue, mConfigurationController, mKeyguardStateController, - mKeyguardUpdateMonitor, mScreenLifecycle, mSysUiState, mProtoTracer, - mWakefulnessLifecycle, mUserTracker, mSysUiMainExecutor); + mCommandQueue, + mConfigurationController, + mKeyguardStateController, + mKeyguardUpdateMonitor, + mScreenLifecycle, + mSysUiState, + mProtoTracer, + mWakefulnessLifecycle, + mUserTracker, + mNoteTaskInitializer, + mSysUiMainExecutor + ); } @Test diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakePromptRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakePromptRepository.kt new file mode 100644 index 000000000000..96658c61109d --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakePromptRepository.kt @@ -0,0 +1,48 @@ +package com.android.systemui.biometrics.data.repository + +import android.hardware.biometrics.PromptInfo +import com.android.systemui.biometrics.data.model.PromptKind +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +/** Fake implementation of [PromptRepository] for tests. */ +class FakePromptRepository : PromptRepository { + + private val _isShowing = MutableStateFlow(false) + override val isShowing = _isShowing.asStateFlow() + + private val _promptInfo = MutableStateFlow<PromptInfo?>(null) + override val promptInfo = _promptInfo.asStateFlow() + + private val _userId = MutableStateFlow<Int?>(null) + override val userId = _userId.asStateFlow() + + private var _challenge = MutableStateFlow<Long?>(null) + override val challenge = _challenge.asStateFlow() + + private val _kind = MutableStateFlow(PromptKind.ANY_BIOMETRIC) + override val kind = _kind.asStateFlow() + + override fun setPrompt( + promptInfo: PromptInfo, + userId: Int, + gatekeeperChallenge: Long?, + kind: PromptKind + ) { + _promptInfo.value = promptInfo + _userId.value = userId + _challenge.value = gatekeeperChallenge + _kind.value = kind + } + + override fun unsetPrompt() { + _promptInfo.value = null + _userId.value = null + _challenge.value = null + _kind.value = PromptKind.ANY_BIOMETRIC + } + + fun setIsShowing(showing: Boolean) { + _isShowing.value = showing + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/domain/interactor/FakeCredentialInteractor.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/domain/interactor/FakeCredentialInteractor.kt new file mode 100644 index 000000000000..fbe291ebaf5d --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/domain/interactor/FakeCredentialInteractor.kt @@ -0,0 +1,31 @@ +package com.android.systemui.biometrics.domain.interactor + +import com.android.internal.widget.LockscreenCredential +import com.android.systemui.biometrics.domain.model.BiometricPromptRequest +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf + +/** Fake implementation of [CredentialInteractor] for tests. */ +class FakeCredentialInteractor : CredentialInteractor { + + /** Sets return value for [isStealthModeActive]. */ + var stealthMode: Boolean = false + + /** Sets return value for [getCredentialOwnerOrSelfId]. */ + var credentialOwnerId: Int? = null + + override fun isStealthModeActive(userId: Int): Boolean = stealthMode + + override fun getCredentialOwnerOrSelfId(userId: Int): Int = credentialOwnerId ?: userId + + override fun verifyCredential( + request: BiometricPromptRequest.Credential, + credential: LockscreenCredential, + ): Flow<CredentialStatus> = verifyCredentialResponse(credential) + + /** Sets the result value for [verifyCredential]. */ + var verifyCredentialResponse: (credential: LockscreenCredential) -> Flow<CredentialStatus> = + { _ -> + flowOf(CredentialStatus.Fail.Error("invalid")) + } +} diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt index 043aff659d6c..b56818693124 100644 --- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt +++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt @@ -15,6 +15,7 @@ */ package com.android.systemui.unfold.progress +import android.os.Trace import android.util.Log import androidx.dynamicanimation.animation.DynamicAnimation import androidx.dynamicanimation.animation.FloatPropertyCompat @@ -117,6 +118,7 @@ class PhysicsBasedUnfoldTransitionProgressProvider( if (DEBUG) { Log.d(TAG, "onFoldUpdate = $update") + Trace.traceCounter(Trace.TRACE_TAG_APP, "fold_update", update) } } diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt index 07473b30dd58..808128d16b7e 100644 --- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt +++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt @@ -16,6 +16,7 @@ package com.android.systemui.unfold.updates import android.os.Handler +import android.os.Trace import android.util.Log import androidx.annotation.FloatRange import androidx.annotation.VisibleForTesting @@ -108,6 +109,7 @@ constructor( private fun onHingeAngle(angle: Float) { if (DEBUG) { Log.d(TAG, "Hinge angle: $angle, lastHingeAngle: $lastHingeAngle") + Trace.traceCounter(Trace.TRACE_TAG_APP, "hinge_angle", angle.toInt()) } val isClosing = angle < lastHingeAngle @@ -115,8 +117,16 @@ constructor( val closingThresholdMet = closingThreshold == null || angle < closingThreshold val isFullyOpened = FULLY_OPEN_DEGREES - angle < FULLY_OPEN_THRESHOLD_DEGREES val closingEventDispatched = lastFoldUpdate == FOLD_UPDATE_START_CLOSING - - if (isClosing && closingThresholdMet && !closingEventDispatched && !isFullyOpened) { + val screenAvailableEventSent = isUnfoldHandled + + if (isClosing // hinge angle should be decreasing since last update + && closingThresholdMet // hinge angle is below certain threshold + && !closingEventDispatched // we haven't sent closing event already + && !isFullyOpened // do not send closing event if we are in fully opened hinge + // angle range as closing threshold could overlap this range + && screenAvailableEventSent // do not send closing event if we are still in + // the process of turning on the inner display + ) { notifyFoldUpdate(FOLD_UPDATE_START_CLOSING) } diff --git a/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java b/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java index 3d24588c9763..9897a0762d9f 100644 --- a/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java +++ b/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java @@ -19,7 +19,6 @@ import android.app.Service; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; -import android.graphics.Camera; import android.graphics.GraphicBuffer; import android.graphics.Rect; import android.hardware.HardwareBuffer; @@ -124,26 +123,31 @@ public class CameraExtensionsProxyService extends Service { private static final String CAMERA_EXTENSION_VERSION_NAME = "androidx.camera.extensions.impl.ExtensionVersionImpl"; - private static final String LATEST_VERSION = "1.3.0"; + private static final String LATEST_VERSION = "1.4.0"; // No support for the init sequence private static final String NON_INIT_VERSION_PREFIX = "1.0"; // Support advanced API and latency queries private static final String ADVANCED_VERSION_PREFIX = "1.2"; // Support for the capture request & result APIs private static final String RESULTS_VERSION_PREFIX = "1.3"; - private static final String[] ADVANCED_VERSION_PREFIXES = {ADVANCED_VERSION_PREFIX, - RESULTS_VERSION_PREFIX}; - private static final String[] SUPPORTED_VERSION_PREFIXES = {RESULTS_VERSION_PREFIX, - ADVANCED_VERSION_PREFIX, "1.1", NON_INIT_VERSION_PREFIX}; + // Support for various latency improvements + private static final String LATENCY_VERSION_PREFIX = "1.4"; + private static final String[] ADVANCED_VERSION_PREFIXES = {LATENCY_VERSION_PREFIX, + ADVANCED_VERSION_PREFIX, RESULTS_VERSION_PREFIX }; + private static final String[] SUPPORTED_VERSION_PREFIXES = {LATENCY_VERSION_PREFIX, + RESULTS_VERSION_PREFIX, ADVANCED_VERSION_PREFIX, "1.1", NON_INIT_VERSION_PREFIX}; private static final boolean EXTENSIONS_PRESENT = checkForExtensions(); private static final String EXTENSIONS_VERSION = EXTENSIONS_PRESENT ? (new ExtensionVersionImpl()).checkApiVersion(LATEST_VERSION) : null; private static final boolean LATENCY_API_SUPPORTED = checkForLatencyAPI(); + private static final boolean LATENCY_IMPROVEMENTS_SUPPORTED = EXTENSIONS_PRESENT && + (EXTENSIONS_VERSION.startsWith(LATENCY_VERSION_PREFIX)); private static final boolean ADVANCED_API_SUPPORTED = checkForAdvancedAPI(); private static final boolean INIT_API_SUPPORTED = EXTENSIONS_PRESENT && (!EXTENSIONS_VERSION.startsWith(NON_INIT_VERSION_PREFIX)); private static final boolean RESULT_API_SUPPORTED = EXTENSIONS_PRESENT && - (EXTENSIONS_VERSION.startsWith(RESULTS_VERSION_PREFIX)); + (EXTENSIONS_VERSION.startsWith(RESULTS_VERSION_PREFIX) || + EXTENSIONS_VERSION.startsWith(LATENCY_VERSION_PREFIX)); private HashMap<String, CameraCharacteristics> mCharacteristicsHashMap = new HashMap<>(); private HashMap<String, Long> mMetadataVendorIdMap = new HashMap<>(); @@ -1169,6 +1173,10 @@ public class CameraExtensionsProxyService extends Service { ret.outputConfigs.add(entry); } ret.sessionTemplateId = sessionConfig.getSessionTemplateId(); + ret.sessionType = -1; + if (LATENCY_IMPROVEMENTS_SUPPORTED) { + ret.sessionType = sessionConfig.getSessionType(); + } ret.sessionParameter = initializeParcelableMetadata( sessionConfig.getSessionParameters(), cameraId); mCameraId = cameraId; @@ -1312,6 +1320,15 @@ public class CameraExtensionsProxyService extends Service { } @Override + public int getSessionType() { + if (LATENCY_IMPROVEMENTS_SUPPORTED) { + return mPreviewExtender.onSessionType(); + } + + return -1; + } + + @Override public int getProcessorType() { ProcessorType processorType = mPreviewExtender.getProcessorType(); if (processorType == ProcessorType.PROCESSOR_TYPE_REQUEST_UPDATE_ONLY) { @@ -1407,6 +1424,15 @@ public class CameraExtensionsProxyService extends Service { } @Override + public int getSessionType() { + if (LATENCY_IMPROVEMENTS_SUPPORTED) { + return mImageExtender.onSessionType(); + } + + return -1; + } + + @Override public void init(String cameraId, CameraMetadataNative chars) { CameraCharacteristics c = new CameraCharacteristics(chars); mCameraManager.registerDeviceStateListener(c); diff --git a/proto/src/task_snapshot.proto b/proto/src/task_snapshot.proto deleted file mode 100644 index 1cbc17ed9f41..000000000000 --- a/proto/src/task_snapshot.proto +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (C) 2017 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. - */ - - syntax = "proto3"; - - package com.android.server.wm; - - option java_package = "com.android.server.wm"; - option java_outer_classname = "WindowManagerProtos"; - - message TaskSnapshotProto { - int32 orientation = 1; - int32 inset_left = 2; - int32 inset_top = 3; - int32 inset_right = 4; - int32 inset_bottom = 5; - bool is_real_snapshot = 6; - int32 windowing_mode = 7; - int32 system_ui_visibility = 8 [deprecated=true]; - bool is_translucent = 9; - string top_activity_component = 10; - // deprecated because original width and height are stored now instead of the scale. - float legacy_scale = 11 [deprecated=true]; - int64 id = 12; - int32 rotation = 13; - // The task width when the snapshot was taken - int32 task_width = 14; - // The task height when the snapshot was taken - int32 task_height = 15; - int32 appearance = 16; - int32 letterbox_inset_left = 17; - int32 letterbox_inset_top = 18; - int32 letterbox_inset_right = 19; - int32 letterbox_inset_bottom = 20; - } diff --git a/proto/src/windowmanager.proto b/proto/src/windowmanager.proto new file mode 100644 index 000000000000..f26404c66623 --- /dev/null +++ b/proto/src/windowmanager.proto @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +syntax = "proto3"; + +package com.android.server.wm; + +option java_package = "com.android.server.wm"; +option java_outer_classname = "WindowManagerProtos"; + +message TaskSnapshotProto { + int32 orientation = 1; + int32 inset_left = 2; + int32 inset_top = 3; + int32 inset_right = 4; + int32 inset_bottom = 5; + bool is_real_snapshot = 6; + int32 windowing_mode = 7; + int32 system_ui_visibility = 8 [deprecated=true]; + bool is_translucent = 9; + string top_activity_component = 10; + // deprecated because original width and height are stored now instead of the scale. + float legacy_scale = 11 [deprecated=true]; + int64 id = 12; + int32 rotation = 13; + // The task width when the snapshot was taken + int32 task_width = 14; + // The task height when the snapshot was taken + int32 task_height = 15; + int32 appearance = 16; + int32 letterbox_inset_left = 17; + int32 letterbox_inset_top = 18; + int32 letterbox_inset_right = 19; + int32 letterbox_inset_bottom = 20; +} + +// Persistent letterboxing configurations +message LetterboxProto { + + // Possible values for the letterbox horizontal reachability + enum LetterboxHorizontalReachability { + LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT = 0; + LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER = 1; + LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT = 2; + } + + // Possible values for the letterbox vertical reachability + enum LetterboxVerticalReachability { + LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP = 0; + LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER = 1; + LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM = 2; + } + + // Represents the current horizontal position for the letterboxed activity + LetterboxHorizontalReachability letterbox_position_for_horizontal_reachability = 1; + // Represents the current vertical position for the letterboxed activity + LetterboxVerticalReachability letterbox_position_for_vertical_reachability = 2; +}
\ No newline at end of file diff --git a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java index 1df382fbd76f..f35de17088d1 100644 --- a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java +++ b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java @@ -176,6 +176,7 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ private boolean mSendMotionEvents; + private SparseArray<Boolean> mServiceDetectsGestures = new SparseArray<>(0); boolean mRequestFilterKeyEvents; boolean mRetrieveInteractiveWindows; @@ -2369,9 +2370,17 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ } public void setServiceDetectsGesturesEnabled(int displayId, boolean mode) { + mServiceDetectsGestures.put(displayId, mode); mSystemSupport.setServiceDetectsGesturesEnabled(displayId, mode); } + public boolean isServiceDetectsGesturesEnabled(int displayId) { + if (mServiceDetectsGestures.contains(displayId)) { + return mServiceDetectsGestures.get(displayId); + } + return false; + } + public void requestTouchExploration(int displayId) { mSystemSupport.requestTouchExploration(displayId); } diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java index 75724bffabf8..d80117d8d8ba 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java @@ -176,6 +176,8 @@ class AccessibilityInputFilter extends InputFilter implements EventStreamTransfo private int mEnabledFeatures; + // Display-specific features + private SparseArray<Boolean> mServiceDetectsGestures = new SparseArray<>(); private final SparseArray<EventStreamState> mMouseStreamStates = new SparseArray<>(0); private final SparseArray<EventStreamState> mTouchScreenStreamStates = new SparseArray<>(0); @@ -458,7 +460,9 @@ class AccessibilityInputFilter extends InputFilter implements EventStreamTransfo final Context displayContext = mContext.createDisplayContext(display); final int displayId = display.getDisplayId(); - + if (!mServiceDetectsGestures.contains(displayId)) { + mServiceDetectsGestures.put(displayId, false); + } if ((mEnabledFeatures & FLAG_FEATURE_AUTOCLICK) != 0) { if (mAutoclickController == null) { mAutoclickController = new AutoclickController( @@ -481,6 +485,7 @@ class AccessibilityInputFilter extends InputFilter implements EventStreamTransfo if ((mEnabledFeatures & FLAG_SEND_MOTION_EVENTS) != 0) { explorer.setSendMotionEventsEnabled(true); } + explorer.setServiceDetectsGestures(mServiceDetectsGestures.get(displayId)); addFirstEventHandler(displayId, explorer); mTouchExplorer.put(displayId, explorer); } @@ -897,6 +902,11 @@ class AccessibilityInputFilter extends InputFilter implements EventStreamTransfo if (mTouchExplorer.contains(displayId)) { mTouchExplorer.get(displayId).setServiceDetectsGestures(mode); } + mServiceDetectsGestures.put(displayId, mode); + } + + public void resetServiceDetectsGestures() { + mServiceDetectsGestures.clear(); } public void requestTouchExploration(int displayId) { diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index 085a58909b6d..47b415630de8 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -1695,31 +1695,34 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } private boolean scheduleNotifyMotionEvent(MotionEvent event) { + boolean result = false; + int displayId = event.getDisplayId(); synchronized (mLock) { AccessibilityUserState state = getCurrentUserStateLocked(); for (int i = state.mBoundServices.size() - 1; i >= 0; i--) { AccessibilityServiceConnection service = state.mBoundServices.get(i); - if (service.mRequestTouchExplorationMode) { + if (service.isServiceDetectsGesturesEnabled(displayId)) { service.notifyMotionEvent(event); - return true; + result = true; } } } - return false; + return result; } private boolean scheduleNotifyTouchState(int displayId, int touchState) { + boolean result = false; synchronized (mLock) { AccessibilityUserState state = getCurrentUserStateLocked(); for (int i = state.mBoundServices.size() - 1; i >= 0; i--) { AccessibilityServiceConnection service = state.mBoundServices.get(i); - if (service.mRequestTouchExplorationMode) { + if (service.isServiceDetectsGesturesEnabled(displayId)) { service.notifyTouchState(displayId, touchState); - return true; + result = true; } } } - return false; + return result; } private void notifyClearAccessibilityCacheLocked() { @@ -2292,8 +2295,9 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub if (!mHasInputFilter) { mHasInputFilter = true; if (mInputFilter == null) { - mInputFilter = new AccessibilityInputFilter(mContext, - AccessibilityManagerService.this); + mInputFilter = + new AccessibilityInputFilter( + mContext, AccessibilityManagerService.this); } inputFilter = mInputFilter; setInputFilter = true; @@ -2303,6 +2307,17 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub if (mHasInputFilter) { mHasInputFilter = false; mInputFilter.setUserAndEnabledFeatures(userState.mUserId, 0); + mInputFilter.resetServiceDetectsGestures(); + if (userState.isTouchExplorationEnabledLocked()) { + // Service gesture detection is turned on and off on a per-display + // basis. + final ArrayList<Display> displays = getValidDisplayList(); + for (Display display : displays) { + int displayId = display.getDisplayId(); + boolean mode = userState.isServiceDetectsGesturesEnabled(displayId); + mInputFilter.setServiceDetectsGesturesEnabled(displayId, mode); + } + } inputFilter = null; setInputFilter = true; } @@ -2618,6 +2633,18 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub Binder.restoreCallingIdentity(identity); } } + // Service gesture detection is turned on and off on a per-display + // basis. + userState.resetServiceDetectsGestures(); + final ArrayList<Display> displays = getValidDisplayList(); + for (AccessibilityServiceConnection service: userState.mBoundServices) { + for (Display display : displays) { + int displayId = display.getDisplayId(); + if (service.isServiceDetectsGesturesEnabled(displayId)) { + userState.setServiceDetectsGesturesEnabled(displayId, true); + } + } + } userState.setServiceHandlesDoubleTapLocked(serviceHandlesDoubleTapEnabled); userState.setMultiFingerGesturesLocked(requestMultiFingerGestures); userState.setTwoFingerPassthroughLocked(requestTwoFingerPassthrough); @@ -4342,6 +4369,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub private void setServiceDetectsGesturesInternal(int displayId, boolean mode) { synchronized (mLock) { + getCurrentUserStateLocked().setServiceDetectsGesturesEnabled(displayId, mode); if (mHasInputFilter && mInputFilter != null) { mInputFilter.setServiceDetectsGesturesEnabled(displayId, mode); } diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java index 0cb7209c187a..0db169fd76c3 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java @@ -44,6 +44,7 @@ import android.provider.Settings; import android.text.TextUtils; import android.util.ArraySet; import android.util.Slog; +import android.util.SparseArray; import android.util.SparseIntArray; import android.view.accessibility.AccessibilityManager; import android.view.accessibility.IAccessibilityManagerClient; @@ -118,6 +119,7 @@ class AccessibilityUserState { private boolean mRequestMultiFingerGestures; private boolean mRequestTwoFingerPassthrough; private boolean mSendMotionEventsEnabled; + private SparseArray<Boolean> mServiceDetectsGestures = new SparseArray<>(0); private int mUserInteractiveUiTimeout; private int mUserNonInteractiveUiTimeout; private int mNonInteractiveUiTimeout = 0; @@ -991,4 +993,19 @@ class AccessibilityUserState { mFocusStrokeWidth = strokeWidth; mFocusColor = color; } + + public void setServiceDetectsGesturesEnabled(int displayId, boolean mode) { + mServiceDetectsGestures.put(displayId, mode); + } + + public void resetServiceDetectsGestures() { + mServiceDetectsGestures.clear(); + } + + public boolean isServiceDetectsGesturesEnabled(int displayId) { + if (mServiceDetectsGestures.contains(displayId)) { + return mServiceDetectsGestures.get(displayId); + } + return false; + } } diff --git a/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java b/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java index fc628cfdced2..000bafe1d650 100644 --- a/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java +++ b/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java @@ -44,6 +44,7 @@ import android.view.Display; import android.window.DisplayWindowPolicyController; import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.app.BlockedAppStreamingActivity; import java.util.List; @@ -112,7 +113,9 @@ public class GenericWindowPolicyController extends DisplayWindowPolicyController final ArraySet<Integer> mRunningUids = new ArraySet<>(); @Nullable private final ActivityListener mActivityListener; private final Handler mHandler = new Handler(Looper.getMainLooper()); - private final ArraySet<RunningAppsChangedListener> mRunningAppsChangedListener = + @NonNull + @GuardedBy("mGenericWindowPolicyControllerLock") + private final ArraySet<RunningAppsChangedListener> mRunningAppsChangedListeners = new ArraySet<>(); @Nullable private final @AssociationRequest.DeviceProfile String mDeviceProfile; @@ -178,12 +181,16 @@ public class GenericWindowPolicyController extends DisplayWindowPolicyController /** Register a listener for running applications changes. */ public void registerRunningAppsChangedListener(@NonNull RunningAppsChangedListener listener) { - mRunningAppsChangedListener.add(listener); + synchronized (mGenericWindowPolicyControllerLock) { + mRunningAppsChangedListeners.add(listener); + } } /** Unregister a listener for running applications changes. */ public void unregisterRunningAppsChangedListener(@NonNull RunningAppsChangedListener listener) { - mRunningAppsChangedListener.remove(listener); + synchronized (mGenericWindowPolicyControllerLock) { + mRunningAppsChangedListeners.remove(listener); + } } @Override @@ -283,12 +290,16 @@ public class GenericWindowPolicyController extends DisplayWindowPolicyController // Post callback on the main thread so it doesn't block activity launching mHandler.post(() -> mActivityListener.onDisplayEmpty(mDisplayId)); } - } - mHandler.post(() -> { - for (RunningAppsChangedListener listener : mRunningAppsChangedListener) { - listener.onRunningAppsChanged(runningUids); + if (!mRunningAppsChangedListeners.isEmpty()) { + final ArraySet<RunningAppsChangedListener> listeners = + new ArraySet<>(mRunningAppsChangedListeners); + mHandler.post(() -> { + for (RunningAppsChangedListener listener : listeners) { + listener.onRunningAppsChanged(runningUids); + } + }); } - }); + } } @Override @@ -354,4 +365,11 @@ public class GenericWindowPolicyController extends DisplayWindowPolicyController } return true; } + + @VisibleForTesting + int getRunningAppsChangedListenersSizeForTesting() { + synchronized (mGenericWindowPolicyControllerLock) { + return mRunningAppsChangedListeners.size(); + } + } } diff --git a/services/core/java/com/android/server/SystemService.java b/services/core/java/com/android/server/SystemService.java index 933d2596aed8..e40f001f27d5 100644 --- a/services/core/java/com/android/server/SystemService.java +++ b/services/core/java/com/android/server/SystemService.java @@ -473,6 +473,18 @@ public abstract class SystemService { } /** + * The {@link UserManager#isUserVisible() user visibility} changed. + * + * <p>This callback is called before the user starts or is switched to (or after it stops), when + * its visibility changed because of that action. + * + * @hide + */ + // NOTE: change visible to int if this method becomes a @SystemApi + public void onUserVisibilityChanged(@NonNull TargetUser user, boolean visible) { + } + + /** * Called when an existing user is stopping, for system services to finalize any per-user * state they maintain for running users. This is called prior to sending the SHUTDOWN * broadcast to the user; it is a good place to stop making use of any resources of that diff --git a/services/core/java/com/android/server/SystemServiceManager.java b/services/core/java/com/android/server/SystemServiceManager.java index 1a8cf0b07cb6..83d86cdc05c6 100644 --- a/services/core/java/com/android/server/SystemServiceManager.java +++ b/services/core/java/com/android/server/SystemServiceManager.java @@ -75,13 +75,17 @@ public final class SystemServiceManager implements Dumpable { // Constants used on onUser(...) // NOTE: do not change their values, as they're used on Trace calls and changes might break // performance tests that rely on them. - private static final String USER_STARTING = "Start"; // Logged as onStartUser - private static final String USER_UNLOCKING = "Unlocking"; // Logged as onUnlockingUser - private static final String USER_UNLOCKED = "Unlocked"; // Logged as onUnlockedUser - private static final String USER_SWITCHING = "Switch"; // Logged as onSwitchUser - private static final String USER_STOPPING = "Stop"; // Logged as onStopUser - private static final String USER_STOPPED = "Cleanup"; // Logged as onCleanupUser - private static final String USER_COMPLETED_EVENT = "CompletedEvent"; // onCompletedEventUser + private static final String USER_STARTING = "Start"; // Logged as onUserStarting() + private static final String USER_UNLOCKING = "Unlocking"; // Logged as onUserUnlocking() + private static final String USER_UNLOCKED = "Unlocked"; // Logged as onUserUnlocked() + private static final String USER_SWITCHING = "Switch"; // Logged as onUserSwitching() + private static final String USER_STOPPING = "Stop"; // Logged as onUserStopping() + private static final String USER_STOPPED = "Cleanup"; // Logged as onUserStopped() + private static final String USER_COMPLETED_EVENT = "CompletedEvent"; // onUserCompletedEvent() + private static final String USER_VISIBLE = "Visible"; // Logged on onUserVisible() and + // onUserStarting() (when visible is true) + private static final String USER_INVISIBLE = "Invisible"; // Logged on onUserStopping() + // (when visibilityChanged is true) // The default number of threads to use if lifecycle thread pool is enabled. private static final int DEFAULT_MAX_USER_POOL_THREADS = 3; @@ -350,18 +354,41 @@ public final class SystemServiceManager implements Dumpable { /** * Starts the given user. */ - public void onUserStarting(@NonNull TimingsTraceAndSlog t, @UserIdInt int userId) { - EventLog.writeEvent(EventLogTags.SSM_USER_STARTING, userId); + public void onUserStarting(@NonNull TimingsTraceAndSlog t, @UserIdInt int userId, + boolean visible) { + EventLog.writeEvent(EventLogTags.SSM_USER_STARTING, userId, visible ? 1 : 0); final TargetUser targetUser = newTargetUser(userId); synchronized (mTargetUsers) { mTargetUsers.put(userId, targetUser); } + if (visible) { + // Must send the user visiiblity change first, for 2 reasons: + // 1. Automotive need to update the user-zone mapping ASAP and it's one of the few + // services listening to this event (OTOH, there are manyy listeners to USER_STARTING + // and some can take a while to process it) + // 2. When a user is switched from bg to fg, the onUserVisibilityChanged() callback is + // called onUserSwitching(), so calling it before onUserStarting() make it more + // consistent with that + onUser(t, USER_VISIBLE, /* prevUser= */ null, targetUser); + } onUser(t, USER_STARTING, /* prevUser= */ null, targetUser); } /** + * Updates the user visibility. + * + * <p><b>NOTE: </b>this method should only be called when a user that is already running become + * visible; if the user is starting visible, callers should call + * {@link #onUserStarting(TimingsTraceAndSlog, int, boolean)} instead + */ + public void onUserVisible(@UserIdInt int userId) { + EventLog.writeEvent(EventLogTags.SSM_USER_VISIBLE, userId); + onUser(USER_VISIBLE, userId); + } + + /** * Unlocks the given user. */ public void onUserUnlocking(@UserIdInt int userId) { @@ -408,9 +435,12 @@ public final class SystemServiceManager implements Dumpable { /** * Stops the given user. */ - public void onUserStopping(@UserIdInt int userId) { - EventLog.writeEvent(EventLogTags.SSM_USER_STOPPING, userId); + public void onUserStopping(@UserIdInt int userId, boolean visibilityChanged) { + EventLog.writeEvent(EventLogTags.SSM_USER_STOPPING, userId, visibilityChanged ? 1 : 0); onUser(USER_STOPPING, userId); + if (visibilityChanged) { + onUser(USER_INVISIBLE, userId); + } } /** @@ -456,13 +486,12 @@ public final class SystemServiceManager implements Dumpable { TargetUser targetUser = getTargetUser(userId); Preconditions.checkState(targetUser != null, "No TargetUser for " + userId); - onUser(TimingsTraceAndSlog.newAsyncLog(), onWhat, /* prevUser= */ null, - targetUser); + onUser(TimingsTraceAndSlog.newAsyncLog(), onWhat, /* prevUser= */ null, targetUser); } private void onUser(@NonNull TimingsTraceAndSlog t, @NonNull String onWhat, @Nullable TargetUser prevUser, @NonNull TargetUser curUser) { - onUser(t, onWhat, prevUser, curUser, /* completedEventType=*/ null); + onUser(t, onWhat, prevUser, curUser, /* completedEventType= */ null); } private void onUser(@NonNull TimingsTraceAndSlog t, @NonNull String onWhat, @@ -534,6 +563,12 @@ public final class SystemServiceManager implements Dumpable { threadPool.submit(getOnUserCompletedEventRunnable( t, service, serviceName, curUser, completedEventType)); break; + case USER_VISIBLE: + service.onUserVisibilityChanged(curUser, /* visible= */ true); + break; + case USER_INVISIBLE: + service.onUserVisibilityChanged(curUser, /* visible= */ false); + break; default: throw new IllegalArgumentException(onWhat + " what?"); } diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index b166adc9a828..5feb3bf5205c 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -470,6 +470,7 @@ import java.util.UUID; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; import java.util.function.BiFunction; public class ActivityManagerService extends IActivityManager.Stub @@ -3455,13 +3456,13 @@ public class ActivityManagerService extends IActivityManager.Stub } /** - * @param firstPidOffsets Optional, when it's set, it receives the start/end offset + * @param firstPidEndOffset Optional, when it's set, it receives the start/end offset * of the very first pid to be dumped. */ /* package */ static File dumpStackTraces(ArrayList<Integer> firstPids, ProcessCpuTracker processCpuTracker, SparseArray<Boolean> lastPids, ArrayList<Integer> nativePids, StringWriter logExceptionCreatingFile, - long[] firstPidOffsets, String subject, String criticalEventSection, + AtomicLong firstPidEndOffset, String subject, String criticalEventSection, AnrLatencyTracker latencyTracker) { try { if (latencyTracker != null) { @@ -3534,15 +3535,10 @@ public class ActivityManagerService extends IActivityManager.Stub + (criticalEventSection != null ? criticalEventSection : "")); } - Pair<Long, Long> offsets = dumpStackTraces( + long firstPidEndPos = dumpStackTraces( tracesFile.getAbsolutePath(), firstPids, nativePids, extraPids, latencyTracker); - if (firstPidOffsets != null) { - if (offsets == null) { - firstPidOffsets[0] = firstPidOffsets[1] = -1; - } else { - firstPidOffsets[0] = offsets.first; // Start offset to the ANR trace file - firstPidOffsets[1] = offsets.second; // End offset to the ANR trace file - } + if (firstPidEndOffset != null) { + firstPidEndOffset.set(firstPidEndPos); } return tracesFile; @@ -3661,9 +3657,9 @@ public class ActivityManagerService extends IActivityManager.Stub /** - * @return The start/end offset of the trace of the very first PID + * @return The end offset of the trace of the very first PID */ - public static Pair<Long, Long> dumpStackTraces(String tracesFile, + public static long dumpStackTraces(String tracesFile, ArrayList<Integer> firstPids, ArrayList<Integer> nativePids, ArrayList<Integer> extraPids, AnrLatencyTracker latencyTracker) { @@ -3679,7 +3675,6 @@ public class ActivityManagerService extends IActivityManager.Stub // As applications are usually interested with the ANR stack traces, but we can't share with // them the stack traces other than their own stacks. So after the very first PID is // dumped, remember the current file size. - long firstPidStart = -1; long firstPidEnd = -1; // First collect all of the stacks of the most important pids. @@ -3692,11 +3687,6 @@ public class ActivityManagerService extends IActivityManager.Stub final int pid = firstPids.get(i); // We don't copy ANR traces from the system_server intentionally. final boolean firstPid = i == 0 && MY_PID != pid; - File tf = null; - if (firstPid) { - tf = new File(tracesFile); - firstPidStart = tf.exists() ? tf.length() : 0; - } if (latencyTracker != null) { latencyTracker.dumpingPidStarted(pid); } @@ -3712,11 +3702,11 @@ public class ActivityManagerService extends IActivityManager.Stub if (remainingTime <= 0) { Slog.e(TAG, "Aborting stack trace dump (current firstPid=" + pid + "); deadline exceeded."); - return firstPidStart >= 0 ? new Pair<>(firstPidStart, firstPidEnd) : null; + return firstPidEnd; } if (firstPid) { - firstPidEnd = tf.length(); + firstPidEnd = new File(tracesFile).length(); // Full latency dump if (latencyTracker != null) { appendtoANRFile(tracesFile, @@ -3755,7 +3745,7 @@ public class ActivityManagerService extends IActivityManager.Stub if (remainingTime <= 0) { Slog.e(TAG, "Aborting stack trace dump (current native pid=" + pid + "); deadline exceeded."); - return firstPidStart >= 0 ? new Pair<>(firstPidStart, firstPidEnd) : null; + return firstPidEnd; } if (DEBUG_ANR) { @@ -3785,7 +3775,7 @@ public class ActivityManagerService extends IActivityManager.Stub if (remainingTime <= 0) { Slog.e(TAG, "Aborting stack trace dump (current extra pid=" + pid + "); deadline exceeded."); - return firstPidStart >= 0 ? new Pair<>(firstPidStart, firstPidEnd) : null; + return firstPidEnd; } if (DEBUG_ANR) { @@ -3800,7 +3790,7 @@ public class ActivityManagerService extends IActivityManager.Stub appendtoANRFile(tracesFile, "----- dumping ended at " + SystemClock.uptimeMillis() + "\n"); Slog.i(TAG, "Done dumping"); - return firstPidStart >= 0 ? new Pair<>(firstPidStart, firstPidEnd) : null; + return firstPidEnd; } @Override @@ -3891,24 +3881,29 @@ public class ActivityManagerService extends IActivityManager.Stub finishForceStopPackageLocked(packageName, appInfo.uid); } } - final Intent intent = new Intent(Intent.ACTION_PACKAGE_DATA_CLEARED, - Uri.fromParts("package", packageName, null)); - intent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND - | Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); - intent.putExtra(Intent.EXTRA_UID, (appInfo != null) ? appInfo.uid : -1); - intent.putExtra(Intent.EXTRA_USER_HANDLE, resolvedUserId); - final int[] visibilityAllowList = - mPackageManagerInt.getVisibilityAllowList(packageName, resolvedUserId); - if (isInstantApp) { - intent.putExtra(Intent.EXTRA_PACKAGE_NAME, packageName); - broadcastIntentInPackage("android", null, SYSTEM_UID, uid, pid, intent, - null, null, null, 0, null, null, permission.ACCESS_INSTANT_APPS, - null, false, false, resolvedUserId, false, null, - visibilityAllowList); - } else { - broadcastIntentInPackage("android", null, SYSTEM_UID, uid, pid, intent, - null, null, null, 0, null, null, null, null, false, false, - resolvedUserId, false, null, visibilityAllowList); + + if (succeeded) { + final Intent intent = new Intent(Intent.ACTION_PACKAGE_DATA_CLEARED, + Uri.fromParts("package", packageName, null /* fragment */)); + intent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND + | Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); + intent.putExtra(Intent.EXTRA_UID, + (appInfo != null) ? appInfo.uid : INVALID_UID); + intent.putExtra(Intent.EXTRA_USER_HANDLE, resolvedUserId); + if (isInstantApp) { + intent.putExtra(Intent.EXTRA_PACKAGE_NAME, packageName); + } + final int[] visibilityAllowList = mPackageManagerInt.getVisibilityAllowList( + packageName, resolvedUserId); + + broadcastIntentInPackage("android", null /* featureId */, + SYSTEM_UID, uid, pid, intent, null /* resolvedType */, + null /* resultToApp */, null /* resultTo */, 0 /* resultCode */, + null /* resultData */, null /* resultExtras */, + isInstantApp ? permission.ACCESS_INSTANT_APPS : null, + null /* bOptions */, false /* serialized */, false /* sticky */, + resolvedUserId, false /* allowBackgroundActivityStarts */, + null /* backgroundActivityStartsToken */, visibilityAllowList); } if (observer != null) { @@ -8346,14 +8341,14 @@ public class ActivityManagerService extends IActivityManager.Stub mBatteryStatsService.noteEvent(BatteryStats.HistoryItem.EVENT_USER_FOREGROUND_START, Integer.toString(currentUserId), currentUserId); - // On Automotive, at this point the system user has already been started and unlocked, - // and some of the tasks we do here have already been done. So skip those in that case. - // TODO(b/132262830, b/203885241): this workdound shouldn't be necessary once we move the - // headless-user start logic to UserManager-land + // On Automotive / Headless System User Mode, at this point the system user has already been + // started and unlocked, and some of the tasks we do here have already been done. So skip + // those in that case. + // TODO(b/242195409): this workaround shouldn't be necessary once we move the headless-user + // start logic to UserManager-land final boolean bootingSystemUser = currentUserId == UserHandle.USER_SYSTEM; - if (bootingSystemUser) { - mSystemServiceManager.onUserStarting(t, currentUserId); + mUserController.onSystemUserStarting(); } synchronized (this) { @@ -13860,6 +13855,9 @@ public class ActivityManagerService extends IActivityManager.Stub @Nullable IBinder backgroundActivityStartsToken, @Nullable int[] broadcastAllowList, @Nullable BiFunction<Integer, Bundle, Bundle> filterExtrasForReceiver) { + // Ensure all internal loopers are registered for idle checks + BroadcastLoopers.addMyLooper(); + if ((resultTo != null) && (resultToApp == null)) { if (resultTo.asBinder() instanceof BinderProxy) { // Warn when requesting results without a way to deliver them @@ -16763,7 +16761,6 @@ public class ActivityManagerService extends IActivityManager.Stub mAtmInternal.onUserStopped(userId); // Clean up various services by removing the user mBatteryStatsService.onUserRemoved(userId); - mUserController.onUserRemoved(userId); } @Override @@ -18110,6 +18107,7 @@ public class ActivityManagerService extends IActivityManager.Stub public void waitForBroadcastIdle(@Nullable PrintWriter pw) { enforceCallingPermission(permission.DUMP, "waitForBroadcastIdle()"); + BroadcastLoopers.waitForIdle(pw); for (BroadcastQueue queue : mBroadcastQueues) { queue.waitForIdle(pw); } @@ -18121,6 +18119,7 @@ public class ActivityManagerService extends IActivityManager.Stub public void waitForBroadcastBarrier(@Nullable PrintWriter pw) { enforceCallingPermission(permission.DUMP, "waitForBroadcastBarrier()"); + BroadcastLoopers.waitForIdle(pw); for (BroadcastQueue queue : mBroadcastQueues) { queue.waitForBarrier(pw); } diff --git a/services/core/java/com/android/server/am/BroadcastConstants.java b/services/core/java/com/android/server/am/BroadcastConstants.java index 4590c859a909..417a0e5ede83 100644 --- a/services/core/java/com/android/server/am/BroadcastConstants.java +++ b/services/core/java/com/android/server/am/BroadcastConstants.java @@ -133,7 +133,7 @@ public class BroadcastConstants { */ public boolean MODERN_QUEUE_ENABLED = DEFAULT_MODERN_QUEUE_ENABLED; private static final String KEY_MODERN_QUEUE_ENABLED = "modern_queue_enabled"; - private static final boolean DEFAULT_MODERN_QUEUE_ENABLED = false; + private static final boolean DEFAULT_MODERN_QUEUE_ENABLED = true; /** * For {@link BroadcastQueueModernImpl}: Maximum number of process queues to diff --git a/services/core/java/com/android/server/am/BroadcastLoopers.java b/services/core/java/com/android/server/am/BroadcastLoopers.java new file mode 100644 index 000000000000..bebb48473fc3 --- /dev/null +++ b/services/core/java/com/android/server/am/BroadcastLoopers.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.am; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Looper; +import android.os.Message; +import android.os.MessageQueue; +import android.os.SystemClock; +import android.util.ArraySet; +import android.util.Slog; + +import java.io.PrintWriter; +import java.util.Objects; +import java.util.concurrent.CountDownLatch; + +/** + * Collection of {@link Looper} that are known to be used for broadcast dispatch + * within the system. This collection can be useful for callers interested in + * confirming that all pending broadcasts have been successfully enqueued. + */ +public class BroadcastLoopers { + private static final String TAG = "BroadcastLoopers"; + + private static final ArraySet<Looper> sLoopers = new ArraySet<>(); + + /** + * Register the given {@link Looper} as possibly having messages that will + * dispatch broadcasts. + */ + public static void addLooper(@NonNull Looper looper) { + synchronized (sLoopers) { + sLoopers.add(Objects.requireNonNull(looper)); + } + } + + /** + * If the current thread is hosting a {@link Looper}, then register it as + * possibly having messages that will dispatch broadcasts. + */ + public static void addMyLooper() { + final Looper looper = Looper.myLooper(); + if (looper != null) { + synchronized (sLoopers) { + if (sLoopers.add(looper)) { + Slog.w(TAG, "Found previously unknown looper " + looper.getThread()); + } + } + } + } + + /** + * Wait for all registered {@link Looper} instances to become idle, as + * defined by {@link MessageQueue#isIdle()}. Note that {@link Message#when} + * still in the future are ignored for the purposes of the idle test. + */ + public static void waitForIdle(@Nullable PrintWriter pw) { + final CountDownLatch latch; + synchronized (sLoopers) { + final int N = sLoopers.size(); + latch = new CountDownLatch(N); + for (int i = 0; i < N; i++) { + final MessageQueue queue = sLoopers.valueAt(i).getQueue(); + if (queue.isIdle()) { + latch.countDown(); + } else { + queue.addIdleHandler(() -> { + latch.countDown(); + return false; + }); + } + } + } + + long lastPrint = 0; + while (latch.getCount() > 0) { + final long now = SystemClock.uptimeMillis(); + if (now >= lastPrint + 1000) { + lastPrint = now; + logv("Waiting for " + latch.getCount() + " loopers to drain...", pw); + } + SystemClock.sleep(100); + } + logv("Loopers drained!", pw); + } + + private static void logv(@NonNull String msg, @Nullable PrintWriter pw) { + Slog.v(TAG, msg); + if (pw != null) { + pw.println(msg); + pw.flush(); + } + } +} diff --git a/services/core/java/com/android/server/am/BroadcastProcessQueue.java b/services/core/java/com/android/server/am/BroadcastProcessQueue.java index 5123517e272d..f7d24e9b8b4e 100644 --- a/services/core/java/com/android/server/am/BroadcastProcessQueue.java +++ b/services/core/java/com/android/server/am/BroadcastProcessQueue.java @@ -85,9 +85,16 @@ class BroadcastProcessQueue { @Nullable ProcessRecord app; /** - * Track name to use for {@link Trace} events. + * Track name to use for {@link Trace} events, defined as part of upgrading + * into a running slot. */ - @Nullable String traceTrackName; + @Nullable String runningTraceTrackName; + + /** + * Flag indicating if this process should be OOM adjusted, defined as part + * of upgrading into a running slot. + */ + boolean runningOomAdjusted; /** * Snapshotted value of {@link ProcessRecord#getCpuDelayTime()}, typically @@ -141,7 +148,8 @@ class BroadcastProcessQueue { private boolean mActiveViaColdStart; /** - * Count of {@link #mPending} broadcasts of these various flavors. + * Count of {@link #mPending} and {@link #mPendingUrgent} broadcasts of + * these various flavors. */ private int mCountForeground; private int mCountOrdered; @@ -150,6 +158,7 @@ class BroadcastProcessQueue { private int mCountInteractive; private int mCountResultTo; private int mCountInstrumented; + private int mCountManifest; private @UptimeMillisLong long mRunnableAt = Long.MAX_VALUE; private @Reason int mRunnableAtReason = REASON_EMPTY; @@ -206,7 +215,7 @@ class BroadcastProcessQueue { // with implicit responsiveness expectations. final ArrayDeque<SomeArgs> queue = record.isUrgent() ? mPendingUrgent : mPending; queue.addLast(newBroadcastArgs); - onBroadcastEnqueued(record); + onBroadcastEnqueued(record, recordIndex); } /** @@ -224,7 +233,8 @@ class BroadcastProcessQueue { while (it.hasNext()) { final SomeArgs args = it.next(); final BroadcastRecord testRecord = (BroadcastRecord) args.arg1; - final Object testReceiver = testRecord.receivers.get(args.argi1); + final int testRecordIndex = args.argi1; + final Object testReceiver = testRecord.receivers.get(testRecordIndex); if ((record.callingUid == testRecord.callingUid) && (record.userId == testRecord.userId) && record.intent.filterEquals(testRecord.intent) @@ -233,8 +243,8 @@ class BroadcastProcessQueue { args.arg1 = record; args.argi1 = recordIndex; args.argi2 = blockedUntilTerminalCount; - onBroadcastDequeued(testRecord); - onBroadcastEnqueued(record); + onBroadcastDequeued(testRecord, testRecordIndex); + onBroadcastEnqueued(record, recordIndex); return true; } } @@ -284,13 +294,13 @@ class BroadcastProcessQueue { while (it.hasNext()) { final SomeArgs args = it.next(); final BroadcastRecord record = (BroadcastRecord) args.arg1; - final int index = args.argi1; - if (predicate.test(record, index)) { - consumer.accept(record, index); + final int recordIndex = args.argi1; + if (predicate.test(record, recordIndex)) { + consumer.accept(record, recordIndex); if (andRemove) { args.recycle(); it.remove(); - onBroadcastDequeued(record); + onBroadcastDequeued(record, recordIndex); } didSomething = true; } @@ -339,7 +349,7 @@ class BroadcastProcessQueue { * Return if we know of an actively running "warm" process for this queue. */ public boolean isProcessWarm() { - return (app != null) && (app.getThread() != null) && !app.isKilled(); + return (app != null) && (app.getOnewayThread() != null) && !app.isKilled(); } public int getPreferredSchedulingGroupLocked() { @@ -385,7 +395,7 @@ class BroadcastProcessQueue { mActiveCountSinceIdle++; mActiveViaColdStart = false; next.recycle(); - onBroadcastDequeued(mActive); + onBroadcastDequeued(mActive, mActiveIndex); } /** @@ -403,7 +413,7 @@ class BroadcastProcessQueue { /** * Update summary statistics when the given record has been enqueued. */ - private void onBroadcastEnqueued(@NonNull BroadcastRecord record) { + private void onBroadcastEnqueued(@NonNull BroadcastRecord record, int recordIndex) { if (record.isForeground()) { mCountForeground++; } @@ -425,13 +435,16 @@ class BroadcastProcessQueue { if (record.callerInstrumented) { mCountInstrumented++; } + if (record.receivers.get(recordIndex) instanceof ResolveInfo) { + mCountManifest++; + } invalidateRunnableAt(); } /** * Update summary statistics when the given record has been dequeued. */ - private void onBroadcastDequeued(@NonNull BroadcastRecord record) { + private void onBroadcastDequeued(@NonNull BroadcastRecord record, int recordIndex) { if (record.isForeground()) { mCountForeground--; } @@ -453,34 +466,37 @@ class BroadcastProcessQueue { if (record.callerInstrumented) { mCountInstrumented--; } + if (record.receivers.get(recordIndex) instanceof ResolveInfo) { + mCountManifest--; + } invalidateRunnableAt(); } public void traceProcessStartingBegin() { Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, - traceTrackName, toShortString() + " starting", hashCode()); + runningTraceTrackName, toShortString() + " starting", hashCode()); } public void traceProcessRunningBegin() { Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, - traceTrackName, toShortString() + " running", hashCode()); + runningTraceTrackName, toShortString() + " running", hashCode()); } public void traceProcessEnd() { Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER, - traceTrackName, hashCode()); + runningTraceTrackName, hashCode()); } public void traceActiveBegin() { final int cookie = mActive.receivers.get(mActiveIndex).hashCode(); Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, - traceTrackName, mActive.toShortString() + " scheduled", cookie); + runningTraceTrackName, mActive.toShortString() + " scheduled", cookie); } public void traceActiveEnd() { final int cookie = mActive.receivers.get(mActiveIndex).hashCode(); Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER, - traceTrackName, cookie); + runningTraceTrackName, cookie); } /** @@ -540,6 +556,14 @@ class BroadcastProcessQueue { } /** + * Quickly determine if this queue has broadcasts waiting to be delivered to + * manifest receivers, which indicates we should request an OOM adjust. + */ + public boolean isPendingManifest() { + return mCountManifest > 0; + } + + /** * Quickly determine if this queue has broadcasts that are still waiting to * be delivered at some point in the future. */ @@ -807,7 +831,7 @@ class BroadcastProcessQueue { @NeverCompile public void dumpLocked(@UptimeMillisLong long now, @NonNull IndentingPrintWriter pw) { - if ((mActive == null) && mPending.isEmpty()) return; + if ((mActive == null) && isEmpty()) return; pw.print(toShortString()); if (isRunnable()) { @@ -823,6 +847,10 @@ class BroadcastProcessQueue { if (mActive != null) { dumpRecord(now, pw, mActive, mActiveIndex, mActiveBlockedUntilTerminalCount); } + for (SomeArgs args : mPendingUrgent) { + final BroadcastRecord r = (BroadcastRecord) args.arg1; + dumpRecord(now, pw, r, args.argi1, args.argi2); + } for (SomeArgs args : mPending) { final BroadcastRecord r = (BroadcastRecord) args.arg1; dumpRecord(now, pw, r, args.argi1, args.argi2); diff --git a/services/core/java/com/android/server/am/BroadcastQueue.md b/services/core/java/com/android/server/am/BroadcastQueue.md new file mode 100644 index 000000000000..81317932ef9b --- /dev/null +++ b/services/core/java/com/android/server/am/BroadcastQueue.md @@ -0,0 +1,98 @@ +# Broadcast Queue Design + +Broadcast intents are one of the major building blocks of the Android platform, +generally intended for asynchronous notification of events. There are three +flavors of intents that can be broadcast: + +* **Normal** broadcast intents are dispatched to relevant receivers. +* **Ordered** broadcast intents are dispatched in a specific order to +receivers, where each receiver has the opportunity to influence the final +"result" of a broadcast, including aborting delivery to any remaining receivers. +* **Sticky** broadcast intents are dispatched to relevant receivers, and are +then retained internally for immediate dispatch to any future receivers. (This +capability has been deprecated and its use is discouraged due to its system +health impact.) + +And there are there two ways to receive these intents: + +* Registered receivers (via `Context.registerReceiver()` methods) are +dynamically requested by a running app to receive intents. These requests are +only maintained while the process is running, and are discarded at process +death. +* Manifest receivers (via the `<receiver>` tag in `AndroidManifest.xml`) are +statically requested by an app to receive intents. These requests are delivered +regardless of process running state, and have the ability to cold-start a +process that isn't currently running. + +## Per-process queues + +The design of `BroadcastQueueModernImpl` is centered around maintaining a +separate `BroadcastProcessQueue` instance for each potential process on the +device. At this level, a process refers to the `android:process` attributes +defined in `AndroidManifest.xml` files, which means it can be defined and +populated regardless of the process state. (For example, a given +`android:process` can have multiple `ProcessRecord`/PIDs defined as it's +launched, killed, and relaunched over long periods of time.) + +Each per-process queue has the concept of a _runnable at_ timestamp when it's +next eligible for execution, and that value can be influenced by a wide range +of policies, such as: + +* Which broadcasts are pending dispatch to a given process. For example, an +"urgent" broadcast typically results in an earlier _runnable at_ time, or a +"delayed" broadcast typically results in a later _runnable at_ time. +* Current state of the process or UID. For example, a "cached" process +typically results in a later _runnable at_ time, or an "instrumented" process +typically results in an earlier _runnable at_ time. +* Blocked waiting for an earlier receiver to complete. For example, an +"ordered" or "prioritized" broadcast typically results in a _not currently +runnable_ value. + +Each per-process queue represents a single remote `ApplicationThread`, and we +only dispatch a single broadcast at a time to each process to ensure developers +see consistent ordering of broadcast events. The flexible _runnable at_ +policies above mean that no inter-process ordering guarantees are provided, +except for those explicitly provided by "ordered" or "prioritized" broadcasts. + +## Parallel dispatch + +Given a collection of per-process queues with valid _runnable at_ timestamps, +BroadcastQueueModernImpl is then willing to promote those _runnable_ queues +into a _running_ state. We choose the next per-process queue to promote based +on the sorted ordering of the _runnable at_ timestamps, selecting the +longest-waiting process first, which aims to reduce overall broadcast dispatch +latency. + +To preserve system health, at most +`BroadcastConstants.MAX_RUNNING_PROCESS_QUEUES` processes are allowed to be in +the _running_ state at any given time, and at most one process is allowed to be +_cold started_ at any given time. (For background, _cold starting_ a process +by forking and specializing the zygote is a relatively heavy operation, so +limiting ourselves to a single pending _cold start_ reduces system-wide +resource contention.) + +After each broadcast is dispatched to a given process, we consider dispatching +any additional pending broadcasts to that process, aimed at batching dispatch +to better amortize the cost of OOM adjustments. + +## Starvation considerations + +Careful attention is given to several types of potential resource starvation, +along with the mechanisms of mitigation: + +* A per-process queue that has a delayed _runnable at_ policy applied can risk +growing very large. This is mitigated by +`BroadcastConstants.MAX_PENDING_BROADCASTS` bypassing any delays when the queue +grows too large. +* A per-process queue that has a large number of pending broadcasts can risk +monopolizing one of the limited _runnable_ slots. This is mitigated by +`BroadcastConstants.MAX_RUNNING_ACTIVE_BROADCASTS` being used to temporarily +"retire" a running process to give other processes a chance to run. +* An "urgent" broadcast dispatched to a process with a large backlog of +"non-urgent" broadcasts can risk large dispatch latencies. This is mitigated +by maintaining a separate `mPendingUrgent` queue of urgent events, which we +prefer to dispatch before the normal `mPending` queue. +* A process with a scheduled broadcast desires to execute, but heavy CPU +contention can risk the process not receiving enough resources before an ANR +timeout is triggered. This is mitigated by extending the "soft" ANR timeout by +up to double the original timeout length. diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java index 4c831bd47ee4..57506199f0e1 100644 --- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java +++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java @@ -58,6 +58,7 @@ import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.os.Bundle; +import android.os.BundleMerger; import android.os.Handler; import android.os.Message; import android.os.Process; @@ -364,6 +365,13 @@ class BroadcastQueueModernImpl extends BroadcastQueue { BroadcastProcessQueue nextQueue = queue.runnableAtNext; final long runnableAt = queue.getRunnableAt(); + // When broadcasts are skipped or failed during list traversal, we + // might encounter a queue that is no longer runnable; skip it + if (!queue.isRunnable()) { + queue = nextQueue; + continue; + } + // If queues beyond this point aren't ready to run yet, schedule // another pass when they'll be runnable if (runnableAt > now && !waitingFor) { @@ -401,24 +409,27 @@ class BroadcastQueueModernImpl extends BroadcastQueue { mRunnableHead = removeFromRunnableList(mRunnableHead, queue); // Emit all trace events for this process into a consistent track - queue.traceTrackName = TAG + ".mRunning[" + queueIndex + "]"; + queue.runningTraceTrackName = TAG + ".mRunning[" + queueIndex + "]"; + queue.runningOomAdjusted = queue.isPendingManifest(); + + // If already warm, we can make OOM adjust request immediately; + // otherwise we need to wait until process becomes warm + if (processWarm) { + notifyStartedRunning(queue); + updateOomAdj |= queue.runningOomAdjusted; + } // If we're already warm, schedule next pending broadcast now; // otherwise we'll wait for the cold start to circle back around queue.makeActiveNextPending(); if (processWarm) { queue.traceProcessRunningBegin(); - notifyStartedRunning(queue); scheduleReceiverWarmLocked(queue); } else { queue.traceProcessStartingBegin(); scheduleReceiverColdLocked(queue); } - // We've moved at least one process into running state above, so we - // need to kick off an OOM adjustment pass - updateOomAdj = true; - // Move to considering next runnable queue queue = nextQueue; } @@ -456,9 +467,13 @@ class BroadcastQueueModernImpl extends BroadcastQueue { // now; dispatch its next broadcast and clear the slot mRunningColdStart = null; + // Now that we're running warm, we can finally request that OOM + // adjust we've been waiting for + notifyStartedRunning(queue); + mService.updateOomAdjPendingTargetsLocked(OOM_ADJ_REASON_START_RECEIVER); + queue.traceProcessEnd(); queue.traceProcessRunningBegin(); - notifyStartedRunning(queue); scheduleReceiverWarmLocked(queue); // We might be willing to kick off another cold start @@ -543,16 +558,7 @@ class BroadcastQueueModernImpl extends BroadcastQueue { }, mBroadcastConsumerSkipAndCanceled, true); } - final int policy = (r.options != null) - ? r.options.getDeliveryGroupPolicy() : BroadcastOptions.DELIVERY_GROUP_POLICY_ALL; - if (policy == BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT) { - forEachMatchingBroadcast(QUEUE_PREDICATE_ANY, (testRecord, testIndex) -> { - // We only allow caller to remove broadcasts they enqueued - return (r.callingUid == testRecord.callingUid) - && (r.userId == testRecord.userId) - && r.matchesDeliveryGroup(testRecord); - }, mBroadcastConsumerSkipAndCanceled, true); - } + applyDeliveryGroupPolicy(r); if (r.isReplacePending()) { // Leave the skipped broadcasts intact in queue, so that we can @@ -609,6 +615,41 @@ class BroadcastQueueModernImpl extends BroadcastQueue { } } + private void applyDeliveryGroupPolicy(@NonNull BroadcastRecord r) { + final int policy = (r.options != null) + ? r.options.getDeliveryGroupPolicy() : BroadcastOptions.DELIVERY_GROUP_POLICY_ALL; + final BroadcastConsumer broadcastConsumer; + switch (policy) { + case BroadcastOptions.DELIVERY_GROUP_POLICY_ALL: + // Older broadcasts need to be left as is in this case, so nothing more to do. + return; + case BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT: + broadcastConsumer = mBroadcastConsumerSkipAndCanceled; + break; + case BroadcastOptions.DELIVERY_GROUP_POLICY_MERGED: + final BundleMerger extrasMerger = r.options.getDeliveryGroupExtrasMerger(); + if (extrasMerger == null) { + // Extras merger is required to be able to merge the extras. So, if it's not + // supplied, then ignore the delivery group policy. + return; + } + broadcastConsumer = (record, recordIndex) -> { + r.intent.mergeExtras(record.intent, extrasMerger); + mBroadcastConsumerSkipAndCanceled.accept(record, recordIndex); + }; + break; + default: + logw("Unknown delivery group policy: " + policy); + return; + } + forEachMatchingBroadcast(QUEUE_PREDICATE_ANY, (testRecord, testIndex) -> { + // We only allow caller to remove broadcasts they enqueued + return (r.callingUid == testRecord.callingUid) + && (r.userId == testRecord.userId) + && r.matchesDeliveryGroup(testRecord); + }, broadcastConsumer, true); + } + /** * Schedule the currently active broadcast on the given queue when we know * the process is cold. This kicks off a cold start and will eventually call @@ -736,7 +777,7 @@ class BroadcastQueueModernImpl extends BroadcastQueue { if (DEBUG_BROADCAST) logv("Scheduling " + r + " to warm " + app); setDeliveryState(queue, app, r, index, receiver, BroadcastRecord.DELIVERY_SCHEDULED); - final IApplicationThread thread = app.getThread(); + final IApplicationThread thread = app.getOnewayThread(); if (thread != null) { try { if (receiver instanceof BroadcastFilter) { @@ -775,9 +816,9 @@ class BroadcastQueueModernImpl extends BroadcastQueue { * ordered broadcast; assumes the sender is still a warm process. */ private void scheduleResultTo(@NonNull BroadcastRecord r) { - if ((r.resultToApp == null) || (r.resultTo == null)) return; + if (r.resultTo == null) return; final ProcessRecord app = r.resultToApp; - final IApplicationThread thread = app.getThread(); + final IApplicationThread thread = (app != null) ? app.getOnewayThread() : null; if (thread != null) { mService.mOomAdjuster.mCachedAppOptimizer.unfreezeTemporarily( app, OOM_ADJ_REASON_FINISH_RECEIVER); @@ -1245,8 +1286,6 @@ class BroadcastQueueModernImpl extends BroadcastQueue { if (queue.app != null) { queue.app.mReceivers.incrementCurReceivers(); - queue.app.mState.forceProcessStateUpTo(ActivityManager.PROCESS_STATE_RECEIVER); - // Don't bump its LRU position if it's in the background restricted. if (mService.mInternal.getRestrictionLevel( queue.uid) < ActivityManager.RESTRICTION_LEVEL_RESTRICTED_BUCKET) { @@ -1256,7 +1295,10 @@ class BroadcastQueueModernImpl extends BroadcastQueue { mService.mOomAdjuster.mCachedAppOptimizer.unfreezeTemporarily(queue.app, OOM_ADJ_REASON_START_RECEIVER); - mService.enqueueOomAdjTargetLocked(queue.app); + if (queue.runningOomAdjusted) { + queue.app.mState.forceProcessStateUpTo(ActivityManager.PROCESS_STATE_RECEIVER); + mService.enqueueOomAdjTargetLocked(queue.app); + } } } @@ -1266,10 +1308,11 @@ class BroadcastQueueModernImpl extends BroadcastQueue { */ private void notifyStoppedRunning(@NonNull BroadcastProcessQueue queue) { if (queue.app != null) { - // Update during our next pass; no need for an immediate update - mService.enqueueOomAdjTargetLocked(queue.app); - queue.app.mReceivers.decrementCurReceivers(); + + if (queue.runningOomAdjusted) { + mService.enqueueOomAdjTargetLocked(queue.app); + } } } diff --git a/services/core/java/com/android/server/am/EventLogTags.logtags b/services/core/java/com/android/server/am/EventLogTags.logtags index d080036733a5..dec8b62de2ec 100644 --- a/services/core/java/com/android/server/am/EventLogTags.logtags +++ b/services/core/java/com/android/server/am/EventLogTags.logtags @@ -101,7 +101,7 @@ option java_package com.android.server.am 30073 uc_finish_user_stopping (userId|1|5) 30074 uc_finish_user_stopped (userId|1|5) 30075 uc_switch_user (userId|1|5) -30076 uc_start_user_internal (userId|1|5) +30076 uc_start_user_internal (userId|1|5),(foreground|1),(displayId|1|5) 30077 uc_unlock_user (userId|1|5) 30078 uc_finish_user_boot (userId|1|5) 30079 uc_dispatch_user_switch (oldUserId|1|5),(newUserId|1|5) @@ -109,13 +109,14 @@ option java_package com.android.server.am 30081 uc_send_user_broadcast (userId|1|5),(IntentAction|3) # Tags below are used by SystemServiceManager - although it's technically part of am, these are # also user switch events and useful to be analyzed together with events above. -30082 ssm_user_starting (userId|1|5) +30082 ssm_user_starting (userId|1|5),(visible|1) 30083 ssm_user_switching (oldUserId|1|5),(newUserId|1|5) 30084 ssm_user_unlocking (userId|1|5) 30085 ssm_user_unlocked (userId|1|5) -30086 ssm_user_stopping (userId|1|5) +30086 ssm_user_stopping (userId|1|5),(visibilityChanged|1) 30087 ssm_user_stopped (userId|1|5) 30088 ssm_user_completed_event (userId|1|5),(eventFlag|1|5) +30089 ssm_user_visible (userId|1|5) # Foreground service start/stop events. 30100 am_foreground_service_start (User|1|5),(Component Name|3),(allowWhileInUse|1),(startReasonCode|3),(targetSdk|1|1),(callerTargetSdk|1|1),(notificationWasDeferred|1),(notificationShown|1),(durationMs|1|3),(startForegroundCount|1|1),(stopReason|3) diff --git a/services/core/java/com/android/server/am/PendingIntentRecord.java b/services/core/java/com/android/server/am/PendingIntentRecord.java index 740efbc658ba..14a169737b38 100644 --- a/services/core/java/com/android/server/am/PendingIntentRecord.java +++ b/services/core/java/com/android/server/am/PendingIntentRecord.java @@ -490,6 +490,11 @@ public final class PendingIntentRecord extends IIntentSender.Stub { final IApplicationThread finishedReceiverThread = caller; boolean sendFinish = finishedReceiver != null; + if ((finishedReceiver != null) && (finishedReceiverThread == null)) { + Slog.w(TAG, "Sending of " + intent + " from " + Binder.getCallingUid() + + " requested resultTo without an IApplicationThread!", new Throwable()); + } + int userId = key.userId; if (userId == UserHandle.USER_CURRENT) { userId = controller.mUserController.getCurrentOrTargetUserId(); diff --git a/services/core/java/com/android/server/am/ProcessErrorStateRecord.java b/services/core/java/com/android/server/am/ProcessErrorStateRecord.java index 71d39964ed72..f461f3d24b5f 100644 --- a/services/core/java/com/android/server/am/ProcessErrorStateRecord.java +++ b/services/core/java/com/android/server/am/ProcessErrorStateRecord.java @@ -62,6 +62,8 @@ import java.io.PrintWriter; import java.io.StringWriter; import java.util.ArrayList; import java.util.UUID; +import java.util.concurrent.atomic.AtomicLong; + /** * The error state of the process, such as if it's crashing/ANR etc. */ @@ -458,10 +460,10 @@ class ProcessErrorStateRecord { // avoid spending 1/2 second collecting stats to rank lastPids. StringWriter tracesFileException = new StringWriter(); // To hold the start and end offset to the ANR trace file respectively. - final long[] offsets = new long[2]; + final AtomicLong firstPidEndOffset = new AtomicLong(-1); File tracesFile = ActivityManagerService.dumpStackTraces(firstPids, isSilentAnr ? null : processCpuTracker, isSilentAnr ? null : lastPids, - nativePids, tracesFileException, offsets, annotation, criticalEventLog, + nativePids, tracesFileException, firstPidEndOffset, annotation, criticalEventLog, latencyTracker); if (isMonitorCpuUsage()) { @@ -478,10 +480,14 @@ class ProcessErrorStateRecord { if (tracesFile == null) { // There is no trace file, so dump (only) the alleged culprit's threads to the log Process.sendSignal(pid, Process.SIGNAL_QUIT); - } else if (offsets[1] > 0) { + } else if (firstPidEndOffset.get() > 0) { // We've dumped into the trace file successfully + // We pass the start and end offsets of the first section of + // the ANR file (the headers and first process dump) + final long startOffset = 0L; + final long endOffset = firstPidEndOffset.get(); mService.mProcessList.mAppExitInfoTracker.scheduleLogAnrTrace( - pid, mApp.uid, mApp.getPackageList(), tracesFile, offsets[0], offsets[1]); + pid, mApp.uid, mApp.getPackageList(), tracesFile, startOffset, endOffset); } // Check if package is still being loaded diff --git a/services/core/java/com/android/server/am/ProcessRecord.java b/services/core/java/com/android/server/am/ProcessRecord.java index 3b04dbb1da98..0a8c6400a6fd 100644 --- a/services/core/java/com/android/server/am/ProcessRecord.java +++ b/services/core/java/com/android/server/am/ProcessRecord.java @@ -54,6 +54,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.app.procstats.ProcessState; import com.android.internal.app.procstats.ProcessStats; import com.android.internal.os.Zygote; +import com.android.server.FgThread; import com.android.server.wm.WindowProcessController; import com.android.server.wm.WindowProcessListener; @@ -143,6 +144,13 @@ class ProcessRecord implements WindowProcessListener { private IApplicationThread mThread; /** + * Instance of {@link #mThread} that will always meet the {@code oneway} + * contract, possibly by using {@link SameProcessApplicationThread}. + */ + @CompositeRWLock({"mService", "mProcLock"}) + private IApplicationThread mOnewayThread; + + /** * Always keep this application running? */ private volatile boolean mPersistent; @@ -603,16 +611,27 @@ class ProcessRecord implements WindowProcessListener { return mThread; } + @GuardedBy(anyOf = {"mService", "mProcLock"}) + IApplicationThread getOnewayThread() { + return mOnewayThread; + } + @GuardedBy({"mService", "mProcLock"}) public void makeActive(IApplicationThread thread, ProcessStatsService tracker) { mProfile.onProcessActive(thread, tracker); mThread = thread; + if (mPid == Process.myPid()) { + mOnewayThread = new SameProcessApplicationThread(thread, FgThread.getHandler()); + } else { + mOnewayThread = thread; + } mWindowProcessController.setThread(thread); } @GuardedBy({"mService", "mProcLock"}) public void makeInactive(ProcessStatsService tracker) { mThread = null; + mOnewayThread = null; mWindowProcessController.setThread(null); mProfile.onProcessInactive(tracker); } diff --git a/services/core/java/com/android/server/am/SameProcessApplicationThread.java b/services/core/java/com/android/server/am/SameProcessApplicationThread.java new file mode 100644 index 000000000000..a3c011188539 --- /dev/null +++ b/services/core/java/com/android/server/am/SameProcessApplicationThread.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.am; + +import android.annotation.NonNull; +import android.app.IApplicationThread; +import android.content.IIntentReceiver; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.res.CompatibilityInfo; +import android.os.Bundle; +import android.os.Handler; +import android.os.RemoteException; + +import java.util.Objects; + +/** + * Wrapper around an {@link IApplicationThread} that delegates selected calls + * through a {@link Handler} so they meet the {@code oneway} contract of + * returning immediately after dispatch. + */ +public class SameProcessApplicationThread extends IApplicationThread.Default { + private final IApplicationThread mWrapped; + private final Handler mHandler; + + public SameProcessApplicationThread(@NonNull IApplicationThread wrapped, + @NonNull Handler handler) { + mWrapped = Objects.requireNonNull(wrapped); + mHandler = Objects.requireNonNull(handler); + } + + @Override + public void scheduleReceiver(Intent intent, ActivityInfo info, CompatibilityInfo compatInfo, + int resultCode, String data, Bundle extras, boolean sync, int sendingUser, + int processState) { + mHandler.post(() -> { + try { + mWrapped.scheduleReceiver(intent, info, compatInfo, resultCode, data, extras, sync, + sendingUser, processState); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + }); + } + + @Override + public void scheduleRegisteredReceiver(IIntentReceiver receiver, Intent intent, int resultCode, + String data, Bundle extras, boolean ordered, boolean sticky, int sendingUser, + int processState) { + mHandler.post(() -> { + try { + mWrapped.scheduleRegisteredReceiver(receiver, intent, resultCode, data, extras, + ordered, sticky, sendingUser, processState); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + }); + } +} diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java index 3c26116e8ad2..82d239f59f1e 100644 --- a/services/core/java/com/android/server/am/UserController.java +++ b/services/core/java/com/android/server/am/UserController.java @@ -100,6 +100,7 @@ import android.util.EventLog; import android.util.IntArray; import android.util.Pair; import android.util.SparseArray; +import android.util.SparseBooleanArray; import android.util.SparseIntArray; import android.util.proto.ProtoOutputStream; import android.view.Display; @@ -119,6 +120,7 @@ import com.android.server.SystemService.UserCompletedEventType; import com.android.server.SystemServiceManager; import com.android.server.am.UserState.KeyEvictedCallback; import com.android.server.pm.UserManagerInternal; +import com.android.server.pm.UserManagerInternal.UserLifecycleListener; import com.android.server.pm.UserManagerService; import com.android.server.utils.Slogf; import com.android.server.utils.TimingsTraceAndSlog; @@ -174,6 +176,9 @@ class UserController implements Handler.Callback { static final int START_USER_SWITCH_FG_MSG = 120; static final int COMPLETE_USER_SWITCH_MSG = 130; static final int USER_COMPLETED_EVENT_MSG = 140; + static final int USER_VISIBLE_MSG = 150; + + private static final int NO_ARG2 = 0; // Message constant to clear {@link UserJourneySession} from {@link mUserIdToUserJourneyMap} if // the user journey, defined in the UserLifecycleJourneyReported atom for statsd, is not @@ -316,8 +321,12 @@ class UserController implements Handler.Callback { @GuardedBy("mLock") private int[] mStartedUserArray = new int[] { 0 }; - // If there are multiple profiles for the current user, their ids are here - // Currently only the primary user can have managed profiles + /** + * Contains the current user and its profiles (if any). + * + * <p><b>NOTE: </b>it lists all profiles, regardless of their running state (i.e., they're in + * this list even if not running). + */ @GuardedBy("mLock") private int[] mCurrentProfileIds = new int[] {}; @@ -421,6 +430,29 @@ class UserController implements Handler.Callback { /** @see #getLastUserUnlockingUptime */ private volatile long mLastUserUnlockingUptime = 0; + /** + * List of visible users (as defined by {@link UserManager#isUserVisible()}). + * + * <p>It's only used to call {@link SystemServiceManager} when the visibility is changed upon + * the user starting or stopping. + * + * <p>Note: only the key is used, not the value. + */ + @GuardedBy("mLock") + private final SparseBooleanArray mVisibleUsers = new SparseBooleanArray(); + + private final UserLifecycleListener mUserLifecycleListener = new UserLifecycleListener() { + @Override + public void onUserCreated(UserInfo user, Object token) { + onUserAdded(user); + } + + @Override + public void onUserRemoved(UserInfo user) { + UserController.this.onUserRemoved(user.id); + } + }; + UserController(ActivityManagerService service) { this(new Injector(service)); } @@ -1050,11 +1082,27 @@ class UserController implements Handler.Callback { // instead. userManagerInternal.unassignUserFromDisplay(userId); + final boolean visibilityChanged; + boolean visibleBefore; + synchronized (mLock) { + visibleBefore = mVisibleUsers.get(userId); + if (visibleBefore) { + if (DEBUG_MU) { + Slogf.d(TAG, "Removing %d from mVisibleUsers", userId); + } + mVisibleUsers.delete(userId); + visibilityChanged = true; + } else { + visibilityChanged = false; + } + } + updateStartedUserArrayLU(); final boolean allowDelayedLockingCopied = allowDelayedLocking; Runnable finishUserStoppingAsync = () -> - mHandler.post(() -> finishUserStopping(userId, uss, allowDelayedLockingCopied)); + mHandler.post(() -> finishUserStopping(userId, uss, allowDelayedLockingCopied, + visibilityChanged)); if (mInjector.getUserManager().isPreCreated(userId)) { finishUserStoppingAsync.run(); @@ -1092,7 +1140,7 @@ class UserController implements Handler.Callback { } private void finishUserStopping(final int userId, final UserState uss, - final boolean allowDelayedLocking) { + final boolean allowDelayedLocking, final boolean visibilityChanged) { EventLog.writeEvent(EventLogTags.UC_FINISH_USER_STOPPING, userId); synchronized (mLock) { if (uss.state != UserState.STATE_STOPPING) { @@ -1109,7 +1157,7 @@ class UserController implements Handler.Callback { mInjector.batteryStatsServiceNoteEvent( BatteryStats.HistoryItem.EVENT_USER_RUNNING_FINISH, Integer.toString(userId), userId); - mInjector.getSystemServiceManager().onUserStopping(userId); + mInjector.getSystemServiceManager().onUserStopping(userId, visibilityChanged); Runnable finishUserStoppedAsync = () -> mHandler.post(() -> finishUserStopped(uss, allowDelayedLocking)); @@ -1513,16 +1561,17 @@ class UserController implements Handler.Callback { private boolean startUserInternal(@UserIdInt int userId, int displayId, boolean foreground, @Nullable IProgressListener unlockListener, @NonNull TimingsTraceAndSlog t) { if (DEBUG_MU) { - Slogf.i(TAG, "Starting user %d on display %d %s", userId, displayId, + Slogf.i(TAG, "Starting user %d on display %d%s", userId, displayId, foreground ? " in foreground" : ""); } - if (displayId != Display.DEFAULT_DISPLAY) { + boolean onSecondaryDisplay = displayId != Display.DEFAULT_DISPLAY; + if (onSecondaryDisplay) { Preconditions.checkArgument(!foreground, "Cannot start user %d in foreground AND " + "on secondary display (%d)", userId, displayId); } - // TODO(b/239982558): log display id (or use a new event) - EventLog.writeEvent(EventLogTags.UC_START_USER_INTERNAL, userId); + EventLog.writeEvent(EventLogTags.UC_START_USER_INTERNAL, userId, foreground ? 1 : 0, + displayId); final int callingUid = Binder.getCallingUid(); final int callingPid = Binder.getCallingPid(); @@ -1571,8 +1620,9 @@ class UserController implements Handler.Callback { return false; } - if (foreground && userInfo.preCreated) { - Slogf.w(TAG, "Cannot start pre-created user #" + userId + " as foreground"); + if ((foreground || onSecondaryDisplay) && userInfo.preCreated) { + Slogf.w(TAG, "Cannot start pre-created user #" + userId + " in foreground or on " + + "secondary display"); return false; } @@ -1634,7 +1684,7 @@ class UserController implements Handler.Callback { userSwitchUiEnabled = mUserSwitchUiEnabled; } mInjector.updateUserConfiguration(); - updateCurrentProfileIds(); + updateProfileRelatedCaches(); mInjector.getWindowManager().setCurrentUser(userId); mInjector.reportCurWakefulnessUsageEvent(); // Once the internal notion of the active user has switched, we lock the device @@ -1648,7 +1698,7 @@ class UserController implements Handler.Callback { } } else { final Integer currentUserIdInt = mCurrentUserId; - updateCurrentProfileIds(); + updateProfileRelatedCaches(); synchronized (mLock) { mUserLru.remove(currentUserIdInt); mUserLru.add(currentUserIdInt); @@ -1656,6 +1706,28 @@ class UserController implements Handler.Callback { } t.traceEnd(); + // Need to call UM when user is on background, as there are some cases where the user + // cannot be started in background on a secondary display (for example, if user is a + // profile). + // TODO(b/253103846): it's also explicitly checking if the user is the USER_SYSTEM, as + // the UM call would return true during boot (when CarService / BootUserInitializer + // calls AM.startUserInBackground() because the system user is still the current user. + // TODO(b/244644281): another fragility of this check is that it must wait to call + // UMI.isUserVisible() until the user state is check, as that method checks if the + // profile of the current user is started. We should fix that dependency so the logic + // belongs to just one place (like UserDisplayAssigner) + boolean visible = foreground + || userId != UserHandle.USER_SYSTEM + && mInjector.getUserManagerInternal().isUserVisible(userId); + if (visible) { + synchronized (mLock) { + if (DEBUG_MU) { + Slogf.d(TAG, "Adding %d to mVisibleUsers", userId); + } + mVisibleUsers.put(userId, true); + } + } + // Make sure user is in the started state. If it is currently // stopping, we need to knock that off. if (uss.state == UserState.STATE_STOPPING) { @@ -1692,8 +1764,15 @@ class UserController implements Handler.Callback { // Booting up a new user, need to tell system services about it. // Note that this is on the same handler as scheduling of broadcasts, // which is important because it needs to go first. - mHandler.sendMessage(mHandler.obtainMessage(USER_START_MSG, userId, 0)); + mHandler.sendMessage(mHandler.obtainMessage(USER_START_MSG, userId, + visible ? 1 : 0)); t.traceEnd(); + } else if (visible) { + // User was already running and became visible (for example, when switching to a + // user that was started in the background before), so it's necessary to explicitly + // notify the services (while when the user starts from BOOTING, USER_START_MSG + // takes care of that. + mHandler.sendMessage(mHandler.obtainMessage(USER_VISIBLE_MSG, userId, NO_ARG2)); } t.traceBegin("sendMessages"); @@ -2110,6 +2189,11 @@ class UserController implements Handler.Callback { mHandler.sendMessage(mHandler.obtainMessage(REPORT_USER_SWITCH_COMPLETE_MSG, newUserId, 0)); stopGuestOrEphemeralUserIfBackground(oldUserId); stopUserOnSwitchIfEnforced(oldUserId); + if (oldUserId == UserHandle.USER_SYSTEM) { + // System user is never stopped, but its visibility is changed (as it is brought to the + // background) + updateSystemUserVisibility(/* visible= */ false); + } t.traceEnd(); // end continueUserSwitch } @@ -2413,9 +2497,7 @@ class UserController implements Handler.Callback { void setAllowUserUnlocking(boolean allowed) { mAllowUserUnlocking = allowed; if (DEBUG_MU) { - // TODO(b/245335748): use Slogf.d instead - // Slogf.d(TAG, new Exception(), "setAllowUserUnlocking(%b)", allowed); - android.util.Slog.d(TAG, "setAllowUserUnlocking():" + allowed, new Exception()); + Slogf.d(TAG, new Exception(), "setAllowUserUnlocking(%b)", allowed); } } @@ -2457,18 +2539,43 @@ class UserController implements Handler.Callback { } void onSystemReady() { - updateCurrentProfileIds(); + if (DEBUG_MU) { + Slogf.d(TAG, "onSystemReady()"); + + } + mInjector.getUserManagerInternal().addUserLifecycleListener(mUserLifecycleListener); + updateProfileRelatedCaches(); mInjector.reportCurWakefulnessUsageEvent(); } + // TODO(b/242195409): remove this method if initial system user boot logic is refactored? + void onSystemUserStarting() { + updateSystemUserVisibility(/* visible= */ !UserManager.isHeadlessSystemUserMode()); + } + + private void updateSystemUserVisibility(boolean visible) { + if (DEBUG_MU) { + Slogf.d(TAG, "updateSystemUserVisibility(): visible=%b", visible); + } + int userId = UserHandle.USER_SYSTEM; + synchronized (mLock) { + if (visible) { + mVisibleUsers.put(userId, true); + } else { + mVisibleUsers.delete(userId); + } + } + mInjector.onUserStarting(userId, visible); + } + /** - * Refreshes the list of users related to the current user when either a - * user switch happens or when a new related user is started in the - * background. + * Refreshes the internal caches related to user profiles. + * + * <p>It's called every time a user is started. */ - private void updateCurrentProfileIds() { + private void updateProfileRelatedCaches() { final List<UserInfo> profiles = mInjector.getUserManager().getProfiles(getCurrentUserId(), - false /* enabledOnly */); + /* enabledOnly= */ false); int[] currentProfileIds = new int[profiles.size()]; // profiles will not be null for (int i = 0; i < currentProfileIds.length; i++) { currentProfileIds[i] = profiles.get(i).id; @@ -2735,6 +2842,18 @@ class UserController implements Handler.Callback { } } + private void onUserAdded(UserInfo user) { + if (!user.isProfile()) { + return; + } + synchronized (mLock) { + if (user.profileGroupId == mCurrentUserId) { + mCurrentProfileIds = ArrayUtils.appendInt(mCurrentProfileIds, user.id); + } + mUserProfileGroupIds.put(user.id, user.profileGroupId); + } + } + void onUserRemoved(@UserIdInt int userId) { synchronized (mLock) { int size = mUserProfileGroupIds.size(); @@ -2846,6 +2965,13 @@ class UserController implements Handler.Callback { proto.end(uToken); } } + for (int i = 0; i < mVisibleUsers.size(); i++) { + proto.write(UserControllerProto.VISIBLE_USERS_ARRAY, mVisibleUsers.keyAt(i)); + } + proto.write(UserControllerProto.CURRENT_USER, mCurrentUserId); + for (int i = 0; i < mCurrentProfileIds.length; i++) { + proto.write(UserControllerProto.CURRENT_PROFILES, mCurrentProfileIds[i]); + } proto.end(token); } } @@ -2883,6 +3009,7 @@ class UserController implements Handler.Callback { pw.println(mUserProfileGroupIds.valueAt(i)); } } + pw.println(" mCurrentProfileIds:" + Arrays.toString(mCurrentProfileIds)); pw.println(" mCurrentUserId:" + mCurrentUserId); pw.println(" mTargetUserId:" + mTargetUserId); pw.println(" mLastActiveUsers:" + mLastActiveUsers); @@ -2899,7 +3026,8 @@ class UserController implements Handler.Callback { if (mSwitchingToSystemUserMessage != null) { pw.println(" mSwitchingToSystemUserMessage: " + mSwitchingToSystemUserMessage); } - pw.println(" mLastUserUnlockingUptime:" + mLastUserUnlockingUptime); + pw.println(" mLastUserUnlockingUptime: " + mLastUserUnlockingUptime); + pw.println(" mVisibleUsers: " + mVisibleUsers); } } @@ -2936,8 +3064,7 @@ class UserController implements Handler.Callback { logUserLifecycleEvent(msg.arg1, USER_LIFECYCLE_EVENT_START_USER, USER_LIFECYCLE_EVENT_STATE_BEGIN); - mInjector.getSystemServiceManager().onUserStarting( - TimingsTraceAndSlog.newAsyncLog(), msg.arg1); + mInjector.onUserStarting(/* userId= */ msg.arg1, /* visible= */ msg.arg2 == 1); scheduleOnUserCompletedEvent(msg.arg1, UserCompletedEventType.EVENT_TYPE_USER_STARTING, USER_COMPLETED_EVENT_DELAY_MS); @@ -3018,6 +3145,9 @@ class UserController implements Handler.Callback { case COMPLETE_USER_SWITCH_MSG: completeUserSwitch(msg.arg1); break; + case USER_VISIBLE_MSG: + mInjector.getSystemServiceManager().onUserVisible(/* userId= */ msg.arg1); + break; } return false; } @@ -3539,5 +3669,10 @@ class UserController implements Handler.Callback { boolean isUsersOnSecondaryDisplaysEnabled() { return UserManager.isUsersOnSecondaryDisplaysEnabled(); } + + void onUserStarting(int userId, boolean visible) { + getSystemServiceManager().onUserStarting(TimingsTraceAndSlog.newAsyncLog(), userId, + visible); + } } } diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java index c59ee83aa895..bbffc894ef3e 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java +++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java @@ -344,7 +344,7 @@ import java.util.concurrent.atomic.AtomicBoolean; if (AudioService.DEBUG_COMM_RTE) { Log.v(TAG, "setCommunicationRouteForClient: device: " + device); } - AudioService.sDeviceLogger.log((new EventLogger.StringEvent( + AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent( "setCommunicationRouteForClient for pid: " + pid + " device: " + device + " from API: " + eventSource)).printLog(TAG)); @@ -1212,7 +1212,7 @@ import java.util.concurrent.atomic.AtomicBoolean; if (useCase == AudioSystem.FOR_MEDIA) { postReportNewRoutes(fromA2dp); } - AudioService.sForceUseLogger.log( + AudioService.sForceUseLogger.enqueue( new AudioServiceEvents.ForceUseEvent(useCase, config, eventSource)); new MediaMetrics.Item(MediaMetrics.Name.AUDIO_FORCE_USE + MediaMetrics.SEPARATOR + AudioSystem.forceUseUsageToString(useCase)) @@ -1230,7 +1230,7 @@ import java.util.concurrent.atomic.AtomicBoolean; } private void onSendBecomingNoisyIntent() { - AudioService.sDeviceLogger.log((new EventLogger.StringEvent( + AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent( "broadcast ACTION_AUDIO_BECOMING_NOISY")).printLog(TAG)); mSystemServer.sendDeviceBecomingNoisyIntent(); } @@ -1468,7 +1468,7 @@ import java.util.concurrent.atomic.AtomicBoolean; case MSG_L_BT_ACTIVE_DEVICE_CHANGE_EXT: { final BtDeviceInfo info = (BtDeviceInfo) msg.obj; if (info.mDevice == null) break; - AudioService.sDeviceLogger.log((new EventLogger.StringEvent( + AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent( "msg: onBluetoothActiveDeviceChange " + " state=" + info.mState // only querying address as this is the only readily available @@ -1858,7 +1858,7 @@ import java.util.concurrent.atomic.AtomicBoolean; Log.v(TAG, "onUpdateCommunicationRoute, preferredCommunicationDevice: " + preferredCommunicationDevice + " eventSource: " + eventSource); } - AudioService.sDeviceLogger.log((new EventLogger.StringEvent( + AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent( "onUpdateCommunicationRoute, preferredCommunicationDevice: " + preferredCommunicationDevice + " eventSource: " + eventSource))); diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java index ce36ff829693..c8f282fd576e 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java +++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java @@ -309,7 +309,7 @@ public class AudioDeviceInventory { address = ""; } - AudioService.sDeviceLogger.log(new EventLogger.StringEvent("BT connected:" + AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent("BT connected:" + " addr=" + address + " profile=" + btInfo.mProfile + " state=" + btInfo.mState @@ -412,13 +412,13 @@ public class AudioDeviceInventory { if (!BluetoothAdapter.checkBluetoothAddress(address)) { address = ""; } - AudioService.sDeviceLogger.log(new EventLogger.StringEvent( + AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( "onBluetoothA2dpDeviceConfigChange addr=" + address + " event=" + BtHelper.a2dpDeviceEventToString(event))); synchronized (mDevicesLock) { if (mDeviceBroker.hasScheduledA2dpConnection(btDevice)) { - AudioService.sDeviceLogger.log(new EventLogger.StringEvent( + AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( "A2dp config change ignored (scheduled connection change)") .printLog(TAG)); mmi.set(MediaMetrics.Property.EARLY_RETURN, "A2dp config change ignored") @@ -460,7 +460,7 @@ public class AudioDeviceInventory { BtHelper.getName(btDevice), a2dpCodec); if (res != AudioSystem.AUDIO_STATUS_OK) { - AudioService.sDeviceLogger.log(new EventLogger.StringEvent( + AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( "APM handleDeviceConfigChange failed for A2DP device addr=" + address + " codec=" + AudioSystem.audioFormatToString(a2dpCodec)) .printLog(TAG)); @@ -472,7 +472,7 @@ public class AudioDeviceInventory { BluetoothProfile.A2DP, BluetoothProfile.STATE_DISCONNECTED, musicDevice, AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP)); } else { - AudioService.sDeviceLogger.log(new EventLogger.StringEvent( + AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( "APM handleDeviceConfigChange success for A2DP device addr=" + address + " codec=" + AudioSystem.audioFormatToString(a2dpCodec)) .printLog(TAG)); @@ -522,7 +522,7 @@ public class AudioDeviceInventory { AudioDeviceInventory.WiredDeviceConnectionState wdcs) { int type = wdcs.mAttributes.getInternalType(); - AudioService.sDeviceLogger.log(new AudioServiceEvents.WiredDevConnectEvent(wdcs)); + AudioService.sDeviceLogger.enqueue(new AudioServiceEvents.WiredDevConnectEvent(wdcs)); MediaMetrics.Item mmi = new MediaMetrics.Item(mMetricsId + "onSetWiredDeviceConnectionState") @@ -619,7 +619,7 @@ public class AudioDeviceInventory { @NonNull List<AudioDeviceAttributes> devices) { final long identity = Binder.clearCallingIdentity(); - AudioService.sDeviceLogger.log((new EventLogger.StringEvent( + AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent( "setPreferredDevicesForStrategySync, strategy: " + strategy + " devices: " + devices)).printLog(TAG)); final int status = mAudioSystem.setDevicesRoleForStrategy( @@ -635,7 +635,7 @@ public class AudioDeviceInventory { /*package*/ int removePreferredDevicesForStrategySync(int strategy) { final long identity = Binder.clearCallingIdentity(); - AudioService.sDeviceLogger.log((new EventLogger.StringEvent( + AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent( "removePreferredDevicesForStrategySync, strategy: " + strategy)).printLog(TAG)); @@ -1000,13 +1000,13 @@ public class AudioDeviceInventory { // TODO: log in MediaMetrics once distinction between connection failure and // double connection is made. if (res != AudioSystem.AUDIO_STATUS_OK) { - AudioService.sDeviceLogger.log(new EventLogger.StringEvent( + AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( "APM failed to make available A2DP device addr=" + address + " error=" + res).printLog(TAG)); // TODO: connection failed, stop here // TODO: return; } else { - AudioService.sDeviceLogger.log(new EventLogger.StringEvent( + AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( "A2DP device addr=" + address + " now available").printLog(TAG)); } @@ -1047,7 +1047,7 @@ public class AudioDeviceInventory { if (!deviceToRemoveKey .equals(mApmConnectedDevices.get(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP))) { // removing A2DP device not currently used by AudioPolicy, log but don't act on it - AudioService.sDeviceLogger.log((new EventLogger.StringEvent( + AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent( "A2DP device " + address + " made unavailable, was not used")).printLog(TAG)); mmi.set(MediaMetrics.Property.EARLY_RETURN, "A2DP device made unavailable, was not used") @@ -1062,13 +1062,13 @@ public class AudioDeviceInventory { AudioSystem.DEVICE_STATE_UNAVAILABLE, a2dpCodec); if (res != AudioSystem.AUDIO_STATUS_OK) { - AudioService.sDeviceLogger.log(new EventLogger.StringEvent( + AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( "APM failed to make unavailable A2DP device addr=" + address + " error=" + res).printLog(TAG)); // TODO: failed to disconnect, stop here // TODO: return; } else { - AudioService.sDeviceLogger.log((new EventLogger.StringEvent( + AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent( "A2DP device addr=" + address + " made unavailable")).printLog(TAG)); } mApmConnectedDevices.remove(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP); @@ -1314,7 +1314,7 @@ public class AudioDeviceInventory { && !mDeviceBroker.hasAudioFocusUsers()) { // no media playback, not a "becoming noisy" situation, otherwise it could cause // the pausing of some apps that are playing remotely - AudioService.sDeviceLogger.log((new EventLogger.StringEvent( + AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent( "dropping ACTION_AUDIO_BECOMING_NOISY")).printLog(TAG)); mmi.set(MediaMetrics.Property.DELAY_MS, 0).record(); // OK to return return 0; diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index f3a9a699b371..9d6fa9ec06bf 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -990,7 +990,7 @@ public class AudioService extends IAudioService.Stub public AudioService(Context context, AudioSystemAdapter audioSystem, SystemServerAdapter systemServer, SettingsAdapter settings, @Nullable Looper looper, AppOpsManager appOps) { - sLifecycleLogger.log(new EventLogger.StringEvent("AudioService()")); + sLifecycleLogger.enqueue(new EventLogger.StringEvent("AudioService()")); mContext = context; mContentResolver = context.getContentResolver(); mAppOps = appOps; @@ -1539,14 +1539,14 @@ public class AudioService extends IAudioService.Stub if (!mSystemReady || (AudioSystem.checkAudioFlinger() != AudioSystem.AUDIO_STATUS_OK)) { Log.e(TAG, "Audioserver died."); - sLifecycleLogger.log(new EventLogger.StringEvent( + sLifecycleLogger.enqueue(new EventLogger.StringEvent( "onAudioServerDied() audioserver died")); sendMsg(mAudioHandler, MSG_AUDIO_SERVER_DIED, SENDMSG_NOOP, 0, 0, null, 500); return; } Log.i(TAG, "Audioserver started."); - sLifecycleLogger.log(new EventLogger.StringEvent( + sLifecycleLogger.enqueue(new EventLogger.StringEvent( "onAudioServerDied() audioserver started")); updateAudioHalPids(); @@ -1776,7 +1776,7 @@ public class AudioService extends IAudioService.Stub // did it work? check based on status if (status != AudioSystem.AUDIO_STATUS_OK) { - sLifecycleLogger.log(new EventLogger.StringEvent( + sLifecycleLogger.enqueue(new EventLogger.StringEvent( caller + ": initStreamVolume failed with " + status + " will retry") .printLog(ALOGE, TAG)); sendMsg(mAudioHandler, MSG_REINIT_VOLUMES, SENDMSG_NOOP, 0, 0, @@ -1790,7 +1790,7 @@ public class AudioService extends IAudioService.Stub } // success - sLifecycleLogger.log(new EventLogger.StringEvent( + sLifecycleLogger.enqueue(new EventLogger.StringEvent( caller + ": initStreamVolume succeeded").printLog(ALOGI, TAG)); } @@ -1813,7 +1813,7 @@ public class AudioService extends IAudioService.Stub } } if (!success) { - sLifecycleLogger.log(new EventLogger.StringEvent( + sLifecycleLogger.enqueue(new EventLogger.StringEvent( caller + ": initStreamVolume succeeded but invalid mix/max levels, will retry") .printLog(ALOGW, TAG)); sendMsg(mAudioHandler, MSG_REINIT_VOLUMES, SENDMSG_NOOP, 0, 0, @@ -2764,7 +2764,7 @@ public class AudioService extends IAudioService.Stub "setPreferredDeviceForStrategy u/pid:%d/%d strat:%d dev:%s", Binder.getCallingUid(), Binder.getCallingPid(), strategy, devices.stream().map(e -> e.toString()).collect(Collectors.joining(","))); - sDeviceLogger.log(new EventLogger.StringEvent(logString).printLog(TAG)); + sDeviceLogger.enqueue(new EventLogger.StringEvent(logString).printLog(TAG)); if (devices.stream().anyMatch(device -> device.getRole() == AudioDeviceAttributes.ROLE_INPUT)) { Log.e(TAG, "Unsupported input routing in " + logString); @@ -2784,7 +2784,7 @@ public class AudioService extends IAudioService.Stub public int removePreferredDevicesForStrategy(int strategy) { final String logString = String.format("removePreferredDeviceForStrategy strat:%d", strategy); - sDeviceLogger.log(new EventLogger.StringEvent(logString).printLog(TAG)); + sDeviceLogger.enqueue(new EventLogger.StringEvent(logString).printLog(TAG)); final int status = mDeviceBroker.removePreferredDevicesForStrategySync(strategy); if (status != AudioSystem.SUCCESS) { @@ -2850,7 +2850,7 @@ public class AudioService extends IAudioService.Stub "setPreferredDevicesForCapturePreset u/pid:%d/%d source:%d dev:%s", Binder.getCallingUid(), Binder.getCallingPid(), capturePreset, devices.stream().map(e -> e.toString()).collect(Collectors.joining(","))); - sDeviceLogger.log(new EventLogger.StringEvent(logString).printLog(TAG)); + sDeviceLogger.enqueue(new EventLogger.StringEvent(logString).printLog(TAG)); if (devices.stream().anyMatch(device -> device.getRole() == AudioDeviceAttributes.ROLE_OUTPUT)) { Log.e(TAG, "Unsupported output routing in " + logString); @@ -2871,7 +2871,7 @@ public class AudioService extends IAudioService.Stub public int clearPreferredDevicesForCapturePreset(int capturePreset) { final String logString = String.format( "removePreferredDeviceForCapturePreset source:%d", capturePreset); - sDeviceLogger.log(new EventLogger.StringEvent(logString).printLog(TAG)); + sDeviceLogger.enqueue(new EventLogger.StringEvent(logString).printLog(TAG)); final int status = mDeviceBroker.clearPreferredDevicesForCapturePresetSync(capturePreset); if (status != AudioSystem.SUCCESS) { @@ -3043,9 +3043,10 @@ public class AudioService extends IAudioService.Stub + ", volControlStream=" + mVolumeControlStream + ", userSelect=" + mUserSelectedVolumeControlStream); if (direction != AudioManager.ADJUST_SAME) { - sVolumeLogger.log(new VolumeEvent(VolumeEvent.VOL_ADJUST_SUGG_VOL, suggestedStreamType, - direction/*val1*/, flags/*val2*/, new StringBuilder(callingPackage) - .append("/").append(caller).append(" uid:").append(uid).toString())); + sVolumeLogger.enqueue( + new VolumeEvent(VolumeEvent.VOL_ADJUST_SUGG_VOL, suggestedStreamType, + direction/*val1*/, flags/*val2*/, new StringBuilder(callingPackage) + .append("/").append(caller).append(" uid:").append(uid).toString())); } boolean hasExternalVolumeController = notifyExternalVolumeController(direction); @@ -3144,7 +3145,7 @@ public class AudioService extends IAudioService.Stub return; } - sVolumeLogger.log(new VolumeEvent(VolumeEvent.VOL_ADJUST_STREAM_VOL, streamType, + sVolumeLogger.enqueue(new VolumeEvent(VolumeEvent.VOL_ADJUST_STREAM_VOL, streamType, direction/*val1*/, flags/*val2*/, callingPackage)); adjustStreamVolume(streamType, direction, flags, callingPackage, callingPackage, Binder.getCallingUid(), Binder.getCallingPid(), attributionTag, @@ -3625,7 +3626,7 @@ public class AudioService extends IAudioService.Stub } final VolumeGroupState vgs = sVolumeGroupStates.get(volumeGroup); - sVolumeLogger.log(new VolumeEvent(VolumeEvent.VOL_SET_GROUP_VOL, attr, vgs.name(), + sVolumeLogger.enqueue(new VolumeEvent(VolumeEvent.VOL_SET_GROUP_VOL, attr, vgs.name(), index/*val1*/, flags/*val2*/, callingPackage)); vgs.setVolumeIndex(index, flags); @@ -3776,7 +3777,7 @@ public class AudioService extends IAudioService.Stub ? new VolumeEvent(VolumeEvent.VOL_SET_STREAM_VOL, streamType, index/*val1*/, flags/*val2*/, callingPackage) : new DeviceVolumeEvent(streamType, index, device, callingPackage); - sVolumeLogger.log(event); + sVolumeLogger.enqueue(event); setStreamVolume(streamType, index, flags, device, callingPackage, callingPackage, attributionTag, Binder.getCallingUid(), callingOrSelfHasAudioSettingsPermission()); @@ -3977,7 +3978,7 @@ public class AudioService extends IAudioService.Stub private void updateHearingAidVolumeOnVoiceActivityUpdate() { final int streamType = getBluetoothContextualVolumeStream(); final int index = getStreamVolume(streamType); - sVolumeLogger.log(new VolumeEvent(VolumeEvent.VOL_VOICE_ACTIVITY_HEARING_AID, + sVolumeLogger.enqueue(new VolumeEvent(VolumeEvent.VOL_VOICE_ACTIVITY_HEARING_AID, mVoicePlaybackActive.get(), streamType, index)); mDeviceBroker.postSetHearingAidVolumeIndex(index * 10, streamType); @@ -4018,7 +4019,7 @@ public class AudioService extends IAudioService.Stub if (AudioSystem.isSingleAudioDeviceType( absVolumeMultiModeCaseDevices, AudioSystem.DEVICE_OUT_HEARING_AID)) { final int index = getStreamVolume(streamType); - sVolumeLogger.log(new VolumeEvent(VolumeEvent.VOL_MODE_CHANGE_HEARING_AID, + sVolumeLogger.enqueue(new VolumeEvent(VolumeEvent.VOL_MODE_CHANGE_HEARING_AID, newMode, streamType, index)); mDeviceBroker.postSetHearingAidVolumeIndex(index * 10, streamType); } @@ -5419,7 +5420,7 @@ public class AudioService extends IAudioService.Stub /*obj*/ null, /*delay*/ 0); int previousMode = mMode.getAndSet(mode); // Note: newModeOwnerPid is always 0 when actualMode is MODE_NORMAL - mModeLogger.log(new PhoneStateEvent(requesterPackage, requesterPid, + mModeLogger.enqueue(new PhoneStateEvent(requesterPackage, requesterPid, requestedMode, pid, mode)); int streamType = getActiveStreamType(AudioManager.USE_DEFAULT_STREAM_TYPE); @@ -5562,7 +5563,7 @@ public class AudioService extends IAudioService.Stub } if (direction != AudioManager.ADJUST_SAME) { - sVolumeLogger.log(new VolumeEvent(VolumeEvent.VOL_ADJUST_VOL_UID, streamType, + sVolumeLogger.enqueue(new VolumeEvent(VolumeEvent.VOL_ADJUST_VOL_UID, streamType, direction/*val1*/, flags/*val2*/, new StringBuilder(packageName).append(" uid:").append(uid) .toString())); @@ -6892,7 +6893,7 @@ public class AudioService extends IAudioService.Stub // verify arguments Objects.requireNonNull(device); AudioManager.enforceValidVolumeBehavior(deviceVolumeBehavior); - sVolumeLogger.log(new EventLogger.StringEvent("setDeviceVolumeBehavior: dev:" + sVolumeLogger.enqueue(new EventLogger.StringEvent("setDeviceVolumeBehavior: dev:" + AudioSystem.getOutputDeviceName(device.getInternalType()) + " addr:" + device.getAddress() + " behavior:" + AudioDeviceVolumeManager.volumeBehaviorName(deviceVolumeBehavior) @@ -6948,7 +6949,7 @@ public class AudioService extends IAudioService.Stub } // log event and caller - sDeviceLogger.log(new EventLogger.StringEvent( + sDeviceLogger.enqueue(new EventLogger.StringEvent( "Volume behavior " + deviceVolumeBehavior + " for dev=0x" + Integer.toHexString(audioSystemDeviceOut) + " from:" + caller)); // make sure we have a volume entry for this device, and that volume is updated according @@ -7594,7 +7595,7 @@ public class AudioService extends IAudioService.Stub final int status = AudioSystem.initStreamVolume( streamType, mIndexMin / 10, mIndexMax / 10); if (status != AudioSystem.AUDIO_STATUS_OK) { - sLifecycleLogger.log(new EventLogger.StringEvent( + sLifecycleLogger.enqueue(new EventLogger.StringEvent( "VSS() stream:" + streamType + " initStreamVolume=" + status) .printLog(ALOGE, TAG)); sendMsg(mAudioHandler, MSG_REINIT_VOLUMES, SENDMSG_NOOP, 0, 0, @@ -8013,7 +8014,7 @@ public class AudioService extends IAudioService.Stub } } if (changed) { - sVolumeLogger.log(new VolumeEvent( + sVolumeLogger.enqueue(new VolumeEvent( VolumeEvent.VOL_MUTE_STREAM_INT, mStreamType, state)); } return changed; @@ -8183,10 +8184,10 @@ public class AudioService extends IAudioService.Stub streamState.setIndex(index, update.mDevice, update.mCaller, // trusted as index is always validated before message is posted true /*hasModifyAudioSettings*/); - sVolumeLogger.log(new EventLogger.StringEvent(update.mCaller + " dev:0x" + sVolumeLogger.enqueue(new EventLogger.StringEvent(update.mCaller + " dev:0x" + Integer.toHexString(update.mDevice) + " volIdx:" + index)); } else { - sVolumeLogger.log(new EventLogger.StringEvent(update.mCaller + sVolumeLogger.enqueue(new EventLogger.StringEvent(update.mCaller + " update vol on dev:0x" + Integer.toHexString(update.mDevice))); } setDeviceVolume(streamState, update.mDevice); @@ -8364,7 +8365,7 @@ public class AudioService extends IAudioService.Stub .set(MediaMetrics.Property.FORCE_USE_MODE, AudioSystem.forceUseConfigToString(config)) .record(); - sForceUseLogger.log( + sForceUseLogger.enqueue( new AudioServiceEvents.ForceUseEvent(useCase, config, eventSource)); mAudioSystem.setForceUse(useCase, config); } @@ -8633,7 +8634,7 @@ public class AudioService extends IAudioService.Stub private void avrcpSupportsAbsoluteVolume(String address, boolean support) { // address is not used for now, but may be used when multiple a2dp devices are supported - sVolumeLogger.log(new EventLogger.StringEvent("avrcpSupportsAbsoluteVolume addr=" + sVolumeLogger.enqueue(new EventLogger.StringEvent("avrcpSupportsAbsoluteVolume addr=" + address + " support=" + support).printLog(TAG)); mDeviceBroker.setAvrcpAbsoluteVolumeSupported(support); setAvrcpAbsoluteVolumeSupported(support); @@ -8767,13 +8768,21 @@ public class AudioService extends IAudioService.Stub UserInfo userInfo = UserManagerService.getInstance().getUserInfo(userId); killBackgroundUserProcessesWithRecordAudioPermission(userInfo); } - UserManagerService.getInstance().setUserRestriction( - UserManager.DISALLOW_RECORD_AUDIO, true, userId); + try { + UserManagerService.getInstance().setUserRestriction( + UserManager.DISALLOW_RECORD_AUDIO, true, userId); + } catch (IllegalArgumentException e) { + Slog.w(TAG, "Failed to apply DISALLOW_RECORD_AUDIO restriction: " + e); + } } else if (action.equals(Intent.ACTION_USER_FOREGROUND)) { // Enable audio recording for foreground user/profile int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1); - UserManagerService.getInstance().setUserRestriction( - UserManager.DISALLOW_RECORD_AUDIO, false, userId); + try { + UserManagerService.getInstance().setUserRestriction( + UserManager.DISALLOW_RECORD_AUDIO, false, userId); + } catch (IllegalArgumentException e) { + Slog.w(TAG, "Failed to apply DISALLOW_RECORD_AUDIO restriction: " + e); + } } else if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) { state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1); if (state == BluetoothAdapter.STATE_OFF || @@ -10668,7 +10677,7 @@ public class AudioService extends IAudioService.Stub pcb.asBinder().linkToDeath(app, 0/*flags*/); // logging after registration so we have the registration id - mDynPolicyLogger.log((new EventLogger.StringEvent("registerAudioPolicy for " + mDynPolicyLogger.enqueue((new EventLogger.StringEvent("registerAudioPolicy for " + pcb.asBinder() + " u/pid:" + Binder.getCallingUid() + "/" + Binder.getCallingPid() + " with config:" + app.toCompactLogString())) .printLog(TAG)); @@ -10866,7 +10875,7 @@ public class AudioService extends IAudioService.Stub private void unregisterAudioPolicyInt(@NonNull IAudioPolicyCallback pcb, String operationName) { - mDynPolicyLogger.log((new EventLogger.StringEvent(operationName + " for " + mDynPolicyLogger.enqueue((new EventLogger.StringEvent(operationName + " for " + pcb.asBinder()).printLog(TAG))); synchronized (mAudioPolicies) { AudioPolicyProxy app = mAudioPolicies.remove(pcb.asBinder()); @@ -11394,7 +11403,7 @@ public class AudioService extends IAudioService.Stub } public void binderDied() { - mDynPolicyLogger.log((new EventLogger.StringEvent("AudioPolicy " + mDynPolicyLogger.enqueue((new EventLogger.StringEvent("AudioPolicy " + mPolicyCallback.asBinder() + " died").printLog(TAG))); release(); } diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java index 399829e32588..df65dbd53e06 100644 --- a/services/core/java/com/android/server/audio/BtHelper.java +++ b/services/core/java/com/android/server/audio/BtHelper.java @@ -263,20 +263,20 @@ public class BtHelper { /*package*/ synchronized void setAvrcpAbsoluteVolumeIndex(int index) { if (mA2dp == null) { if (AudioService.DEBUG_VOL) { - AudioService.sVolumeLogger.log(new EventLogger.StringEvent( + AudioService.sVolumeLogger.enqueue(new EventLogger.StringEvent( "setAvrcpAbsoluteVolumeIndex: bailing due to null mA2dp").printLog(TAG)); return; } } if (!mAvrcpAbsVolSupported) { - AudioService.sVolumeLogger.log(new EventLogger.StringEvent( + AudioService.sVolumeLogger.enqueue(new EventLogger.StringEvent( "setAvrcpAbsoluteVolumeIndex: abs vol not supported ").printLog(TAG)); return; } if (AudioService.DEBUG_VOL) { Log.i(TAG, "setAvrcpAbsoluteVolumeIndex index=" + index); } - AudioService.sVolumeLogger.log(new AudioServiceEvents.VolumeEvent( + AudioService.sVolumeLogger.enqueue(new AudioServiceEvents.VolumeEvent( AudioServiceEvents.VolumeEvent.VOL_SET_AVRCP_VOL, index)); mA2dp.setAvrcpAbsoluteVolume(index); } @@ -393,14 +393,14 @@ public class BtHelper { @GuardedBy("AudioDeviceBroker.mDeviceStateLock") /*package*/ synchronized boolean startBluetoothSco(int scoAudioMode, @NonNull String eventSource) { - AudioService.sDeviceLogger.log(new EventLogger.StringEvent(eventSource)); + AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(eventSource)); return requestScoState(BluetoothHeadset.STATE_AUDIO_CONNECTED, scoAudioMode); } // @GuardedBy("AudioDeviceBroker.mSetModeLock") @GuardedBy("AudioDeviceBroker.mDeviceStateLock") /*package*/ synchronized boolean stopBluetoothSco(@NonNull String eventSource) { - AudioService.sDeviceLogger.log(new EventLogger.StringEvent(eventSource)); + AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(eventSource)); return requestScoState(BluetoothHeadset.STATE_AUDIO_DISCONNECTED, SCO_MODE_VIRTUAL_CALL); } @@ -418,7 +418,7 @@ public class BtHelper { Log.i(TAG, "setLeAudioVolume: calling mLeAudio.setVolume idx=" + index + " volume=" + volume); } - AudioService.sVolumeLogger.log(new AudioServiceEvents.VolumeEvent( + AudioService.sVolumeLogger.enqueue(new AudioServiceEvents.VolumeEvent( AudioServiceEvents.VolumeEvent.VOL_SET_LE_AUDIO_VOL, index, maxIndex)); mLeAudio.setVolume(volume); } @@ -443,7 +443,7 @@ public class BtHelper { } // do not log when hearing aid is not connected to avoid confusion when reading dumpsys if (isHeadAidConnected) { - AudioService.sVolumeLogger.log(new AudioServiceEvents.VolumeEvent( + AudioService.sVolumeLogger.enqueue(new AudioServiceEvents.VolumeEvent( AudioServiceEvents.VolumeEvent.VOL_SET_HEARING_AID_VOL, index, gainDB)); } mHearingAid.setVolume(gainDB); @@ -675,7 +675,7 @@ public class BtHelper { case BluetoothProfile.HEADSET: case BluetoothProfile.HEARING_AID: case BluetoothProfile.LE_AUDIO: - AudioService.sDeviceLogger.log(new EventLogger.StringEvent( + AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( "BT profile service: connecting " + BluetoothProfile.getProfileName(profile) + " profile")); mDeviceBroker.postBtProfileConnected(profile, proxy); diff --git a/services/core/java/com/android/server/audio/FadeOutManager.java b/services/core/java/com/android/server/audio/FadeOutManager.java index e54ee869fef2..5f6f4b125710 100644 --- a/services/core/java/com/android/server/audio/FadeOutManager.java +++ b/services/core/java/com/android/server/audio/FadeOutManager.java @@ -245,7 +245,7 @@ public final class FadeOutManager { return; } try { - PlaybackActivityMonitor.sEventLogger.log( + PlaybackActivityMonitor.sEventLogger.enqueue( (new PlaybackActivityMonitor.FadeOutEvent(apc, skipRamp)).printLog(TAG)); apc.getPlayerProxy().applyVolumeShaper( FADEOUT_VSHAPE, @@ -262,7 +262,7 @@ public final class FadeOutManager { final AudioPlaybackConfiguration apc = players.get(piid); if (apc != null) { try { - PlaybackActivityMonitor.sEventLogger.log( + PlaybackActivityMonitor.sEventLogger.enqueue( (new EventLogger.StringEvent("unfading out piid:" + piid)).printLog(TAG)); apc.getPlayerProxy().applyVolumeShaper( diff --git a/services/core/java/com/android/server/audio/MediaFocusControl.java b/services/core/java/com/android/server/audio/MediaFocusControl.java index 1ca27dd7112c..27687b2612e5 100644 --- a/services/core/java/com/android/server/audio/MediaFocusControl.java +++ b/services/core/java/com/android/server/audio/MediaFocusControl.java @@ -185,7 +185,7 @@ public class MediaFocusControl implements PlayerFocusEnforcer { final FocusRequester focusOwner = stackIterator.next(); if (focusOwner.hasSameUid(uid) && focusOwner.hasSamePackage(packageName)) { clientsToRemove.add(focusOwner.getClientId()); - mEventLogger.log((new EventLogger.StringEvent( + mEventLogger.enqueue((new EventLogger.StringEvent( "focus owner:" + focusOwner.getClientId() + " in uid:" + uid + " pack: " + packageName + " getting AUDIOFOCUS_LOSS due to app suspension")) @@ -433,7 +433,7 @@ public class MediaFocusControl implements PlayerFocusEnforcer { FocusRequester fr = stackIterator.next(); if(fr.hasSameBinder(cb)) { Log.i(TAG, "AudioFocus removeFocusStackEntryOnDeath(): removing entry for " + cb); - mEventLogger.log(new EventLogger.StringEvent( + mEventLogger.enqueue(new EventLogger.StringEvent( "focus requester:" + fr.getClientId() + " in uid:" + fr.getClientUid() + " pack:" + fr.getPackageName() @@ -470,7 +470,7 @@ public class MediaFocusControl implements PlayerFocusEnforcer { final FocusRequester fr = owner.getValue(); if (fr.hasSameBinder(cb)) { ownerIterator.remove(); - mEventLogger.log(new EventLogger.StringEvent( + mEventLogger.enqueue(new EventLogger.StringEvent( "focus requester:" + fr.getClientId() + " in uid:" + fr.getClientUid() + " pack:" + fr.getPackageName() @@ -968,7 +968,7 @@ public class MediaFocusControl implements PlayerFocusEnforcer { // supposed to be alone in bitfield final int uid = (flags == AudioManager.AUDIOFOCUS_FLAG_TEST) ? testUid : Binder.getCallingUid(); - mEventLogger.log((new EventLogger.StringEvent( + mEventLogger.enqueue((new EventLogger.StringEvent( "requestAudioFocus() from uid/pid " + uid + "/" + Binder.getCallingPid() + " AA=" + aa.usageToString() + "/" + aa.contentTypeToString() @@ -1143,7 +1143,7 @@ public class MediaFocusControl implements PlayerFocusEnforcer { .record(); // AudioAttributes are currently ignored, to be used for zones / a11y - mEventLogger.log((new EventLogger.StringEvent( + mEventLogger.enqueue((new EventLogger.StringEvent( "abandonAudioFocus() from uid/pid " + Binder.getCallingUid() + "/" + Binder.getCallingPid() + " clientId=" + clientId)) diff --git a/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java b/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java index 1af8c593f96b..74bfa80e4704 100644 --- a/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java +++ b/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java @@ -157,7 +157,7 @@ public final class PlaybackActivityMonitor if (index >= 0) { if (!disable) { if (DEBUG) { // hidden behind DEBUG, too noisy otherwise - sEventLogger.log(new EventLogger.StringEvent("unbanning uid:" + uid)); + sEventLogger.enqueue(new EventLogger.StringEvent("unbanning uid:" + uid)); } mBannedUids.remove(index); // nothing else to do, future playback requests from this uid are ok @@ -168,7 +168,7 @@ public final class PlaybackActivityMonitor checkBanPlayer(apc, uid); } if (DEBUG) { // hidden behind DEBUG, too noisy otherwise - sEventLogger.log(new EventLogger.StringEvent("banning uid:" + uid)); + sEventLogger.enqueue(new EventLogger.StringEvent("banning uid:" + uid)); } mBannedUids.add(new Integer(uid)); } // no else to handle, uid already not in list, so enabling again is no-op @@ -209,7 +209,7 @@ public final class PlaybackActivityMonitor updateAllowedCapturePolicy(apc, mAllowedCapturePolicies.get(uid)); } } - sEventLogger.log(new NewPlayerEvent(apc)); + sEventLogger.enqueue(new NewPlayerEvent(apc)); synchronized(mPlayerLock) { mPlayers.put(newPiid, apc); maybeMutePlayerAwaitingConnection(apc); @@ -229,7 +229,7 @@ public final class PlaybackActivityMonitor synchronized(mPlayerLock) { final AudioPlaybackConfiguration apc = mPlayers.get(new Integer(piid)); if (checkConfigurationCaller(piid, apc, binderUid)) { - sEventLogger.log(new AudioAttrEvent(piid, attr)); + sEventLogger.enqueue(new AudioAttrEvent(piid, attr)); change = apc.handleAudioAttributesEvent(attr); } else { Log.e(TAG, "Error updating audio attributes"); @@ -322,7 +322,7 @@ public final class PlaybackActivityMonitor return; } - sEventLogger.log(new PlayerEvent(piid, event, eventValue)); + sEventLogger.enqueue(new PlayerEvent(piid, event, eventValue)); if (event == AudioPlaybackConfiguration.PLAYER_UPDATE_PORT_ID) { mEventHandler.sendMessage( @@ -332,7 +332,7 @@ public final class PlaybackActivityMonitor for (Integer uidInteger: mBannedUids) { if (checkBanPlayer(apc, uidInteger.intValue())) { // player was banned, do not update its state - sEventLogger.log(new EventLogger.StringEvent( + sEventLogger.enqueue(new EventLogger.StringEvent( "not starting piid:" + piid + " ,is banned")); return; } @@ -412,7 +412,7 @@ public final class PlaybackActivityMonitor public void playerHasOpPlayAudio(int piid, boolean hasOpPlayAudio, int binderUid) { // no check on UID yet because this is only for logging at the moment - sEventLogger.log(new PlayerOpPlayAudioEvent(piid, hasOpPlayAudio, binderUid)); + sEventLogger.enqueue(new PlayerOpPlayAudioEvent(piid, hasOpPlayAudio, binderUid)); } public void releasePlayer(int piid, int binderUid) { @@ -421,7 +421,7 @@ public final class PlaybackActivityMonitor synchronized(mPlayerLock) { final AudioPlaybackConfiguration apc = mPlayers.get(new Integer(piid)); if (checkConfigurationCaller(piid, apc, binderUid)) { - sEventLogger.log(new EventLogger.StringEvent( + sEventLogger.enqueue(new EventLogger.StringEvent( "releasing player piid:" + piid)); mPlayers.remove(new Integer(piid)); mDuckingManager.removeReleased(apc); @@ -443,7 +443,7 @@ public final class PlaybackActivityMonitor } /*package*/ void onAudioServerDied() { - sEventLogger.log( + sEventLogger.enqueue( new EventLogger.StringEvent( "clear port id to piid map")); synchronized (mPlayerLock) { @@ -768,7 +768,7 @@ public final class PlaybackActivityMonitor } if (mute) { try { - sEventLogger.log((new EventLogger.StringEvent("call: muting piid:" + sEventLogger.enqueue((new EventLogger.StringEvent("call: muting piid:" + piid + " uid:" + apc.getClientUid())).printLog(TAG)); apc.getPlayerProxy().setVolume(0.0f); mMutedPlayers.add(new Integer(piid)); @@ -793,7 +793,7 @@ public final class PlaybackActivityMonitor final AudioPlaybackConfiguration apc = mPlayers.get(piid); if (apc != null) { try { - sEventLogger.log(new EventLogger.StringEvent("call: unmuting piid:" + sEventLogger.enqueue(new EventLogger.StringEvent("call: unmuting piid:" + piid).printLog(TAG)); apc.getPlayerProxy().setVolume(1.0f); } catch (Exception e) { @@ -1081,7 +1081,7 @@ public final class PlaybackActivityMonitor return; } try { - sEventLogger.log((new DuckEvent(apc, skipRamp)).printLog(TAG)); + sEventLogger.enqueue((new DuckEvent(apc, skipRamp)).printLog(TAG)); apc.getPlayerProxy().applyVolumeShaper( DUCK_VSHAPE, skipRamp ? PLAY_SKIP_RAMP : PLAY_CREATE_IF_NEEDED); @@ -1096,7 +1096,7 @@ public final class PlaybackActivityMonitor final AudioPlaybackConfiguration apc = players.get(piid); if (apc != null) { try { - sEventLogger.log((new EventLogger.StringEvent("unducking piid:" + sEventLogger.enqueue((new EventLogger.StringEvent("unducking piid:" + piid)).printLog(TAG)); apc.getPlayerProxy().applyVolumeShaper( DUCK_ID, @@ -1310,8 +1310,9 @@ public final class PlaybackActivityMonitor //========================================================================================== void muteAwaitConnection(@NonNull int[] usagesToMute, @NonNull AudioDeviceAttributes dev, long timeOutMs) { - sEventLogger.loglogi( - "muteAwaitConnection() dev:" + dev + " timeOutMs:" + timeOutMs, TAG); + sEventLogger.enqueueAndLog( + "muteAwaitConnection() dev:" + dev + " timeOutMs:" + timeOutMs, + EventLogger.Event.ALOGI, TAG); synchronized (mPlayerLock) { mutePlayersExpectingDevice(usagesToMute); // schedule timeout (remove previously scheduled first) @@ -1323,7 +1324,8 @@ public final class PlaybackActivityMonitor } void cancelMuteAwaitConnection(String source) { - sEventLogger.loglogi("cancelMuteAwaitConnection() from:" + source, TAG); + sEventLogger.enqueueAndLog("cancelMuteAwaitConnection() from:" + source, + EventLogger.Event.ALOGI, TAG); synchronized (mPlayerLock) { // cancel scheduled timeout, ignore device, only one expected device at a time mEventHandler.removeMessages(MSG_L_TIMEOUT_MUTE_AWAIT_CONNECTION); @@ -1346,7 +1348,7 @@ public final class PlaybackActivityMonitor @GuardedBy("mPlayerLock") private void mutePlayersExpectingDevice(@NonNull int[] usagesToMute) { - sEventLogger.log(new MuteAwaitConnectionEvent(usagesToMute)); + sEventLogger.enqueue(new MuteAwaitConnectionEvent(usagesToMute)); mMutedUsagesAwaitingConnection = usagesToMute; final Set<Integer> piidSet = mPlayers.keySet(); final Iterator<Integer> piidIterator = piidSet.iterator(); @@ -1369,7 +1371,7 @@ public final class PlaybackActivityMonitor for (int usage : mMutedUsagesAwaitingConnection) { if (usage == apc.getAudioAttributes().getUsage()) { try { - sEventLogger.log((new EventLogger.StringEvent( + sEventLogger.enqueue((new EventLogger.StringEvent( "awaiting connection: muting piid:" + apc.getPlayerInterfaceId() + " uid:" + apc.getClientUid())).printLog(TAG)); @@ -1394,7 +1396,7 @@ public final class PlaybackActivityMonitor continue; } try { - sEventLogger.log(new EventLogger.StringEvent( + sEventLogger.enqueue(new EventLogger.StringEvent( "unmuting piid:" + piid).printLog(TAG)); apc.getPlayerProxy().applyVolumeShaper(MUTE_AWAIT_CONNECTION_VSHAPE, VolumeShaper.Operation.REVERSE); @@ -1452,8 +1454,9 @@ public final class PlaybackActivityMonitor public void handleMessage(Message msg) { switch (msg.what) { case MSG_L_TIMEOUT_MUTE_AWAIT_CONNECTION: - sEventLogger.loglogi("Timeout for muting waiting for " - + (AudioDeviceAttributes) msg.obj + ", unmuting", TAG); + sEventLogger.enqueueAndLog("Timeout for muting waiting for " + + (AudioDeviceAttributes) msg.obj + ", unmuting", + EventLogger.Event.ALOGI, TAG); synchronized (mPlayerLock) { unmutePlayersExpectingDevice(); } @@ -1476,7 +1479,7 @@ public final class PlaybackActivityMonitor synchronized (mPlayerLock) { int piid = msg.arg1; - sEventLogger.log( + sEventLogger.enqueue( new PlayerEvent(piid, PLAYER_UPDATE_MUTED, eventValue)); final AudioPlaybackConfiguration apc = mPlayers.get(piid); diff --git a/services/core/java/com/android/server/audio/RecordingActivityMonitor.java b/services/core/java/com/android/server/audio/RecordingActivityMonitor.java index 2ba8882ae14f..652ea5228571 100644 --- a/services/core/java/com/android/server/audio/RecordingActivityMonitor.java +++ b/services/core/java/com/android/server/audio/RecordingActivityMonitor.java @@ -164,7 +164,7 @@ public final class RecordingActivityMonitor implements AudioSystem.AudioRecordin } if (MediaRecorder.isSystemOnlyAudioSource(source)) { // still want to log event, it just won't appear in recording configurations; - sEventLogger.log(new RecordingEvent(event, riid, config).printLog(TAG)); + sEventLogger.enqueue(new RecordingEvent(event, riid, config).printLog(TAG)); return; } dispatchCallbacks(updateSnapshot(event, riid, config)); @@ -204,7 +204,7 @@ public final class RecordingActivityMonitor implements AudioSystem.AudioRecordin ? AudioManager.RECORD_CONFIG_EVENT_STOP : AudioManager.RECORD_CONFIG_EVENT_NONE; if (riid == AudioManager.RECORD_RIID_INVALID || configEvent == AudioManager.RECORD_CONFIG_EVENT_NONE) { - sEventLogger.log(new RecordingEvent(event, riid, null).printLog(TAG)); + sEventLogger.enqueue(new RecordingEvent(event, riid, null).printLog(TAG)); return; } dispatchCallbacks(updateSnapshot(configEvent, riid, null)); @@ -301,7 +301,7 @@ public final class RecordingActivityMonitor implements AudioSystem.AudioRecordin if (!state.hasDeathHandler()) { if (state.isActiveConfiguration()) { configChanged = true; - sEventLogger.log(new RecordingEvent( + sEventLogger.enqueue(new RecordingEvent( AudioManager.RECORD_CONFIG_EVENT_RELEASE, state.getRiid(), state.getConfig())); } @@ -486,7 +486,7 @@ public final class RecordingActivityMonitor implements AudioSystem.AudioRecordin configChanged = false; } if (configChanged) { - sEventLogger.log(new RecordingEvent(event, riid, state.getConfig())); + sEventLogger.enqueue(new RecordingEvent(event, riid, state.getConfig())); configs = getActiveRecordingConfigurations(true /*isPrivileged*/); } } diff --git a/services/core/java/com/android/server/audio/SoundEffectsHelper.java b/services/core/java/com/android/server/audio/SoundEffectsHelper.java index 93eba50ac6dd..79b54ebfeb3c 100644 --- a/services/core/java/com/android/server/audio/SoundEffectsHelper.java +++ b/services/core/java/com/android/server/audio/SoundEffectsHelper.java @@ -164,7 +164,7 @@ class SoundEffectsHelper { } private void logEvent(String msg) { - mSfxLogger.log(new EventLogger.StringEvent(msg)); + mSfxLogger.enqueue(new EventLogger.StringEvent(msg)); } // All the methods below run on the worker thread diff --git a/services/core/java/com/android/server/audio/SpatializerHelper.java b/services/core/java/com/android/server/audio/SpatializerHelper.java index 1563d33d93f0..2b525f1fcf50 100644 --- a/services/core/java/com/android/server/audio/SpatializerHelper.java +++ b/services/core/java/com/android/server/audio/SpatializerHelper.java @@ -1708,11 +1708,11 @@ public class SpatializerHelper { private static void loglogi(String msg) { - AudioService.sSpatialLogger.loglogi(msg, TAG); + AudioService.sSpatialLogger.enqueueAndLog(msg, EventLogger.Event.ALOGI, TAG); } private static String logloge(String msg) { - AudioService.sSpatialLogger.loglog(msg, EventLogger.Event.ALOGE, TAG); + AudioService.sSpatialLogger.enqueueAndLog(msg, EventLogger.Event.ALOGE, TAG); return msg; } diff --git a/services/core/java/com/android/server/biometrics/sensors/SensorOverlays.java b/services/core/java/com/android/server/biometrics/sensors/SensorOverlays.java index aeb6b6e2a907..969a174f49c7 100644 --- a/services/core/java/com/android/server/biometrics/sensors/SensorOverlays.java +++ b/services/core/java/com/android/server/biometrics/sensors/SensorOverlays.java @@ -20,6 +20,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.hardware.biometrics.BiometricOverlayConstants; import android.hardware.fingerprint.ISidefpsController; +import android.hardware.fingerprint.IUdfpsOverlay; import android.hardware.fingerprint.IUdfpsOverlayController; import android.hardware.fingerprint.IUdfpsOverlayControllerCallback; import android.os.RemoteException; @@ -43,6 +44,7 @@ public final class SensorOverlays { @NonNull private final Optional<IUdfpsOverlayController> mUdfpsOverlayController; @NonNull private final Optional<ISidefpsController> mSidefpsController; + @NonNull private final Optional<IUdfpsOverlay> mUdfpsOverlay; /** * Create an overlay controller for each modality. @@ -52,9 +54,11 @@ public final class SensorOverlays { */ public SensorOverlays( @Nullable IUdfpsOverlayController udfpsOverlayController, - @Nullable ISidefpsController sidefpsController) { + @Nullable ISidefpsController sidefpsController, + @Nullable IUdfpsOverlay udfpsOverlay) { mUdfpsOverlayController = Optional.ofNullable(udfpsOverlayController); mSidefpsController = Optional.ofNullable(sidefpsController); + mUdfpsOverlay = Optional.ofNullable(udfpsOverlay); } /** @@ -90,6 +94,14 @@ public final class SensorOverlays { Slog.e(TAG, "Remote exception when showing the UDFPS overlay", e); } } + + if (mUdfpsOverlay.isPresent()) { + try { + mUdfpsOverlay.get().show(client.getRequestId(), sensorId, reason); + } catch (RemoteException e) { + Slog.e(TAG, "Remote exception when showing the new UDFPS overlay", e); + } + } } /** @@ -113,6 +125,14 @@ public final class SensorOverlays { Slog.e(TAG, "Remote exception when hiding the UDFPS overlay", e); } } + + if (mUdfpsOverlay.isPresent()) { + try { + mUdfpsOverlay.get().hide(sensorId); + } catch (RemoteException e) { + Slog.e(TAG, "Remote exception when hiding the new udfps overlay", e); + } + } } /** diff --git a/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java b/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java index 2761ec04aa7e..7a5b58413014 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java @@ -600,8 +600,9 @@ public class FaceService extends SystemService { } try { final SensorProps[] props = face.getSensorProps(); - final FaceProvider provider = new FaceProvider(getContext(), props, instance, - mLockoutResetDispatcher, BiometricContext.getInstance(getContext())); + final FaceProvider provider = new FaceProvider(getContext(), + mBiometricStateCallback, props, instance, mLockoutResetDispatcher, + BiometricContext.getInstance(getContext())); providers.add(provider); } catch (RemoteException e) { Slog.e(TAG, "Remote exception in getSensorProps: " + fqName); @@ -612,14 +613,14 @@ public class FaceService extends SystemService { } @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL) - @Override // Binder call public void registerAuthenticators( @NonNull List<FaceSensorPropertiesInternal> hidlSensors) { mRegistry.registerAll(() -> { final List<ServiceProvider> providers = new ArrayList<>(); for (FaceSensorPropertiesInternal hidlSensor : hidlSensors) { providers.add( - Face10.newInstance(getContext(), hidlSensor, mLockoutResetDispatcher)); + Face10.newInstance(getContext(), mBiometricStateCallback, + hidlSensor, mLockoutResetDispatcher)); } providers.addAll(getAidlProviders()); return providers; diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/BiometricTestSessionImpl.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/BiometricTestSessionImpl.java index 73c272f7a779..cfbb5dce4c2b 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/BiometricTestSessionImpl.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/BiometricTestSessionImpl.java @@ -16,8 +16,6 @@ package com.android.server.biometrics.sensors.face.aidl; -import static android.Manifest.permission.TEST_BIOMETRIC; - import android.annotation.NonNull; import android.content.Context; import android.hardware.biometrics.ITestSession; @@ -33,7 +31,6 @@ import android.os.RemoteException; import android.util.Slog; import com.android.server.biometrics.HardwareAuthTokenUtils; -import com.android.server.biometrics.Utils; import com.android.server.biometrics.sensors.BaseClientMonitor; import com.android.server.biometrics.sensors.ClientMonitorCallback; import com.android.server.biometrics.sensors.face.FaceUtils; diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java index c12994c993e6..6488185c727d 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java @@ -52,9 +52,11 @@ import com.android.server.biometrics.log.BiometricContext; import com.android.server.biometrics.log.BiometricLogger; import com.android.server.biometrics.sensors.AuthenticationClient; import com.android.server.biometrics.sensors.BaseClientMonitor; +import com.android.server.biometrics.sensors.BiometricStateCallback; import com.android.server.biometrics.sensors.BiometricScheduler; import com.android.server.biometrics.sensors.ClientMonitorCallback; import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter; +import com.android.server.biometrics.sensors.ClientMonitorCompositeCallback; import com.android.server.biometrics.sensors.InvalidationRequesterClient; import com.android.server.biometrics.sensors.LockoutResetDispatcher; import com.android.server.biometrics.sensors.PerformanceTracker; @@ -81,6 +83,7 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { private boolean mTestHalEnabled; @NonNull private final Context mContext; + @NonNull private final BiometricStateCallback mBiometricStateCallback; @NonNull private final String mHalInstanceName; @NonNull @VisibleForTesting final SparseArray<Sensor> mSensors; // Map of sensors that this HAL supports @@ -122,11 +125,14 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { } } - public FaceProvider(@NonNull Context context, @NonNull SensorProps[] props, + public FaceProvider(@NonNull Context context, + @NonNull BiometricStateCallback biometricStateCallback, + @NonNull SensorProps[] props, @NonNull String halInstanceName, @NonNull LockoutResetDispatcher lockoutResetDispatcher, @NonNull BiometricContext biometricContext) { mContext = context; + mBiometricStateCallback = biometricStateCallback; mHalInstanceName = halInstanceName; mSensors = new SparseArray<>(); mHandler = new Handler(Looper.getMainLooper()); @@ -363,16 +369,18 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { createLogger(BiometricsProtoEnums.ACTION_ENROLL, BiometricsProtoEnums.CLIENT_UNKNOWN), mBiometricContext, maxTemplatesPerUser, debugConsent); - scheduleForSensor(sensorId, client, new ClientMonitorCallback() { - @Override - public void onClientFinished(@NonNull BaseClientMonitor clientMonitor, - boolean success) { - if (success) { - scheduleLoadAuthenticatorIdsForUser(sensorId, userId); - scheduleInvalidationRequest(sensorId, userId); - } - } - }); + scheduleForSensor(sensorId, client, new ClientMonitorCompositeCallback( + mBiometricStateCallback, new ClientMonitorCallback() { + @Override + public void onClientFinished(@NonNull BaseClientMonitor clientMonitor, + boolean success) { + ClientMonitorCallback.super.onClientFinished(clientMonitor, success); + if (success) { + scheduleLoadAuthenticatorIdsForUser(sensorId, userId); + scheduleInvalidationRequest(sensorId, userId); + } + } + })); }); return id; } @@ -396,7 +404,7 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { token, id, callback, userId, opPackageName, sensorId, createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient), mBiometricContext, isStrongBiometric); - scheduleForSensor(sensorId, client); + scheduleForSensor(sensorId, client, mBiometricStateCallback); }); return id; @@ -424,7 +432,7 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { mBiometricContext, isStrongBiometric, mUsageStats, mSensors.get(sensorId).getLockoutCache(), allowBackgroundAuthentication, isKeyguardBypassEnabled, biometricStrength); - scheduleForSensor(sensorId, client); + scheduleForSensor(sensorId, client, mBiometricStateCallback); }); } @@ -479,7 +487,7 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { BiometricsProtoEnums.CLIENT_UNKNOWN), mBiometricContext, mSensors.get(sensorId).getAuthenticatorIds()); - scheduleForSensor(sensorId, client); + scheduleForSensor(sensorId, client, mBiometricStateCallback); }); } @@ -568,7 +576,8 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { if (favorHalEnrollments) { client.setFavorHalEnrollments(); } - scheduleForSensor(sensorId, client, callback); + scheduleForSensor(sensorId, client, new ClientMonitorCompositeCallback(callback, + mBiometricStateCallback)); }); } diff --git a/services/core/java/com/android/server/biometrics/sensors/face/hidl/BiometricTestSessionImpl.java b/services/core/java/com/android/server/biometrics/sensors/face/hidl/BiometricTestSessionImpl.java index 14af216a9dc5..7a6a274f8dd7 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/hidl/BiometricTestSessionImpl.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/hidl/BiometricTestSessionImpl.java @@ -16,8 +16,6 @@ package com.android.server.biometrics.sensors.face.hidl; -import static android.Manifest.permission.TEST_BIOMETRIC; - import android.annotation.NonNull; import android.content.Context; import android.hardware.biometrics.ITestSession; @@ -30,7 +28,6 @@ import android.os.Binder; import android.os.RemoteException; import android.util.Slog; -import com.android.server.biometrics.Utils; import com.android.server.biometrics.sensors.BaseClientMonitor; import com.android.server.biometrics.sensors.ClientMonitorCallback; import com.android.server.biometrics.sensors.face.FaceUtils; @@ -53,6 +50,7 @@ public class BiometricTestSessionImpl extends ITestSession.Stub { @NonNull private final Set<Integer> mEnrollmentIds; @NonNull private final Random mRandom; + private final IFaceServiceReceiver mReceiver = new IFaceServiceReceiver.Stub() { @Override public void onEnrollResult(Face face, int remaining) { @@ -116,7 +114,8 @@ public class BiometricTestSessionImpl extends ITestSession.Stub { }; BiometricTestSessionImpl(@NonNull Context context, int sensorId, - @NonNull ITestSessionCallback callback, @NonNull Face10 face10, + @NonNull ITestSessionCallback callback, + @NonNull Face10 face10, @NonNull Face10.HalResultController halResultController) { mContext = context; mSensorId = sensorId; diff --git a/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java b/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java index c0a119ff5f1e..0e0ee1966024 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java @@ -62,8 +62,10 @@ import com.android.server.biometrics.sensors.AuthenticationConsumer; import com.android.server.biometrics.sensors.BaseClientMonitor; import com.android.server.biometrics.sensors.BiometricNotificationUtils; import com.android.server.biometrics.sensors.BiometricScheduler; +import com.android.server.biometrics.sensors.BiometricStateCallback; import com.android.server.biometrics.sensors.ClientMonitorCallback; import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter; +import com.android.server.biometrics.sensors.ClientMonitorCompositeCallback; import com.android.server.biometrics.sensors.EnumerateConsumer; import com.android.server.biometrics.sensors.ErrorConsumer; import com.android.server.biometrics.sensors.LockoutResetDispatcher; @@ -110,6 +112,7 @@ public class Face10 implements IHwBinder.DeathRecipient, ServiceProvider { private boolean mTestHalEnabled; @NonNull private final FaceSensorPropertiesInternal mSensorProperties; + @NonNull private final BiometricStateCallback mBiometricStateCallback; @NonNull private final Context mContext; @NonNull private final BiometricScheduler mScheduler; @NonNull private final Handler mHandler; @@ -336,6 +339,7 @@ public class Face10 implements IHwBinder.DeathRecipient, ServiceProvider { @VisibleForTesting Face10(@NonNull Context context, + @NonNull BiometricStateCallback biometricStateCallback, @NonNull FaceSensorPropertiesInternal sensorProps, @NonNull LockoutResetDispatcher lockoutResetDispatcher, @NonNull Handler handler, @@ -343,6 +347,7 @@ public class Face10 implements IHwBinder.DeathRecipient, ServiceProvider { @NonNull BiometricContext biometricContext) { mSensorProperties = sensorProps; mContext = context; + mBiometricStateCallback = biometricStateCallback; mSensorId = sensorProps.sensorId; mScheduler = scheduler; mHandler = handler; @@ -366,11 +371,12 @@ public class Face10 implements IHwBinder.DeathRecipient, ServiceProvider { } public static Face10 newInstance(@NonNull Context context, + @NonNull BiometricStateCallback biometricStateCallback, @NonNull FaceSensorPropertiesInternal sensorProps, @NonNull LockoutResetDispatcher lockoutResetDispatcher) { final Handler handler = new Handler(Looper.getMainLooper()); - return new Face10(context, sensorProps, lockoutResetDispatcher, handler, - new BiometricScheduler(TAG, BiometricScheduler.SENSOR_TYPE_FACE, + return new Face10(context, biometricStateCallback, sensorProps, lockoutResetDispatcher, + handler, new BiometricScheduler(TAG, BiometricScheduler.SENSOR_TYPE_FACE, null /* gestureAvailabilityTracker */), BiometricContext.getInstance(context)); } @@ -615,8 +621,19 @@ public class Face10 implements IHwBinder.DeathRecipient, ServiceProvider { mScheduler.scheduleClientMonitor(client, new ClientMonitorCallback() { @Override + public void onClientStarted(@NonNull BaseClientMonitor clientMonitor) { + mBiometricStateCallback.onClientStarted(clientMonitor); + } + + @Override + public void onBiometricAction(int action) { + mBiometricStateCallback.onBiometricAction(action); + } + + @Override public void onClientFinished(@NonNull BaseClientMonitor clientMonitor, boolean success) { + mBiometricStateCallback.onClientFinished(clientMonitor, success); if (success) { // Update authenticatorIds scheduleUpdateActiveUserWithoutHandler(client.getTargetUserId()); @@ -661,7 +678,7 @@ public class Face10 implements IHwBinder.DeathRecipient, ServiceProvider { createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient), mBiometricContext, isStrongBiometric, mLockoutTracker, mUsageStats, allowBackgroundAuthentication, isKeyguardBypassEnabled); - mScheduler.scheduleClientMonitor(client); + mScheduler.scheduleClientMonitor(client, mBiometricStateCallback); }); } @@ -696,7 +713,7 @@ public class Face10 implements IHwBinder.DeathRecipient, ServiceProvider { createLogger(BiometricsProtoEnums.ACTION_REMOVE, BiometricsProtoEnums.CLIENT_UNKNOWN), mBiometricContext, mAuthenticatorIds); - mScheduler.scheduleClientMonitor(client); + mScheduler.scheduleClientMonitor(client, mBiometricStateCallback); }); } @@ -714,7 +731,7 @@ public class Face10 implements IHwBinder.DeathRecipient, ServiceProvider { createLogger(BiometricsProtoEnums.ACTION_REMOVE, BiometricsProtoEnums.CLIENT_UNKNOWN), mBiometricContext, mAuthenticatorIds); - mScheduler.scheduleClientMonitor(client); + mScheduler.scheduleClientMonitor(client, mBiometricStateCallback); }); } @@ -806,14 +823,15 @@ public class Face10 implements IHwBinder.DeathRecipient, ServiceProvider { BiometricsProtoEnums.CLIENT_UNKNOWN), mBiometricContext, enrolledList, FaceUtils.getLegacyInstance(mSensorId), mAuthenticatorIds); - mScheduler.scheduleClientMonitor(client, callback); + mScheduler.scheduleClientMonitor(client, new ClientMonitorCompositeCallback(callback, + mBiometricStateCallback)); }); } @Override public void scheduleInternalCleanup(int sensorId, int userId, @Nullable ClientMonitorCallback callback) { - scheduleInternalCleanup(userId, callback); + scheduleInternalCleanup(userId, mBiometricStateCallback); } @Override @@ -1011,7 +1029,7 @@ public class Face10 implements IHwBinder.DeathRecipient, ServiceProvider { @Override public ITestSession createTestSession(int sensorId, @NonNull ITestSessionCallback callback, @NonNull String opPackageName) { - return new BiometricTestSessionImpl(mContext, mSensorId, callback, this, - mHalResultController); + return new BiometricTestSessionImpl(mContext, mSensorId, callback, + this, mHalResultController); } } diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java index b0dc28ddce96..156e6bb503ec 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java @@ -52,6 +52,7 @@ import android.hardware.fingerprint.IFingerprintClientActiveCallback; import android.hardware.fingerprint.IFingerprintService; import android.hardware.fingerprint.IFingerprintServiceReceiver; import android.hardware.fingerprint.ISidefpsController; +import android.hardware.fingerprint.IUdfpsOverlay; import android.hardware.fingerprint.IUdfpsOverlayController; import android.os.Binder; import android.os.Build; @@ -874,6 +875,14 @@ public class FingerprintService extends SystemService { @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL) @Override + public void setUdfpsOverlay(@NonNull IUdfpsOverlay controller) { + for (ServiceProvider provider : mRegistry.getProviders()) { + provider.setUdfpsOverlay(controller); + } + } + + @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL) + @Override public void onPowerPressed() { for (ServiceProvider provider : mRegistry.getProviders()) { provider.onPowerPressed(); diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/ServiceProvider.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/ServiceProvider.java index 0c29f5615b4c..05c2e2919a11 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/ServiceProvider.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/ServiceProvider.java @@ -26,6 +26,7 @@ import android.hardware.fingerprint.FingerprintManager; import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; import android.hardware.fingerprint.IFingerprintServiceReceiver; import android.hardware.fingerprint.ISidefpsController; +import android.hardware.fingerprint.IUdfpsOverlay; import android.hardware.fingerprint.IUdfpsOverlayController; import android.os.IBinder; @@ -129,6 +130,12 @@ public interface ServiceProvider extends void setUdfpsOverlayController(@NonNull IUdfpsOverlayController controller); + /** + * Sets udfps overlay + * @param controller udfps overlay + */ + void setUdfpsOverlay(@NonNull IUdfpsOverlay controller); + void onPowerPressed(); /** diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java index 2e5663db57b5..7f1fb1cfc4bd 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java @@ -33,6 +33,7 @@ import android.hardware.biometrics.common.OperationContext; import android.hardware.biometrics.fingerprint.PointerContext; import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; import android.hardware.fingerprint.ISidefpsController; +import android.hardware.fingerprint.IUdfpsOverlay; import android.hardware.fingerprint.IUdfpsOverlayController; import android.os.Build; import android.os.Handler; @@ -118,6 +119,7 @@ class FingerprintAuthenticationClient extends AuthenticationClient<AidlSession> @NonNull LockoutCache lockoutCache, @Nullable IUdfpsOverlayController udfpsOverlayController, @Nullable ISidefpsController sidefpsController, + @Nullable IUdfpsOverlay udfpsOverlay, boolean allowBackgroundAuthentication, @NonNull FingerprintSensorPropertiesInternal sensorProps, @NonNull Handler handler, @@ -145,7 +147,8 @@ class FingerprintAuthenticationClient extends AuthenticationClient<AidlSession> false /* isKeyguardBypassEnabled */); setRequestId(requestId); mLockoutCache = lockoutCache; - mSensorOverlays = new SensorOverlays(udfpsOverlayController, sidefpsController); + mSensorOverlays = new SensorOverlays(udfpsOverlayController, + sidefpsController, udfpsOverlay); mSensorProps = sensorProps; mALSProbeCallback = getLogger().getAmbientLightProbe(false /* startWithClient */); mHandler = handler; @@ -248,8 +251,8 @@ class FingerprintAuthenticationClient extends AuthenticationClient<AidlSession> if (authenticated && mSensorProps.isAnySidefpsType()) { if (mHandler.hasMessages(MESSAGE_IGNORE_AUTH)) { Slog.i(TAG, "(sideFPS) Ignoring auth due to recent power press"); - onErrorInternal(BiometricConstants.BIOMETRIC_ERROR_POWER_PRESSED, 0, - true); + onErrorInternal(BiometricConstants.BIOMETRIC_ERROR_POWER_PRESSED, + 0, true); return; } delay = isKeyguard() ? mWaitForAuthKeyguard : mWaitForAuthBp; diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java index 0e89814c6ad2..52822347e96f 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java @@ -21,6 +21,7 @@ import android.annotation.Nullable; import android.content.Context; import android.hardware.biometrics.BiometricOverlayConstants; import android.hardware.biometrics.common.ICancellationSignal; +import android.hardware.fingerprint.IUdfpsOverlay; import android.hardware.fingerprint.IUdfpsOverlayController; import android.os.IBinder; import android.os.RemoteException; @@ -54,12 +55,15 @@ class FingerprintDetectClient extends AcquisitionClient<AidlSession> implements @NonNull ClientMonitorCallbackConverter listener, int userId, @NonNull String owner, int sensorId, @NonNull BiometricLogger biometricLogger, @NonNull BiometricContext biometricContext, - @Nullable IUdfpsOverlayController udfpsOverlayController, boolean isStrongBiometric) { + @Nullable IUdfpsOverlayController udfpsOverlayController, + @Nullable IUdfpsOverlay udfpsOverlay, + boolean isStrongBiometric) { super(context, lazyDaemon, token, listener, userId, owner, 0 /* cookie */, sensorId, true /* shouldVibrate */, biometricLogger, biometricContext); setRequestId(requestId); mIsStrongBiometric = isStrongBiometric; - mSensorOverlays = new SensorOverlays(udfpsOverlayController, null /* sideFpsController*/); + mSensorOverlays = new SensorOverlays(udfpsOverlayController, + null /* sideFpsController*/, udfpsOverlay); } @Override @@ -82,7 +86,8 @@ class FingerprintDetectClient extends AcquisitionClient<AidlSession> implements @Override protected void startHalOperation() { - mSensorOverlays.show(getSensorId(), BiometricOverlayConstants.REASON_AUTH_KEYGUARD, this); + mSensorOverlays.show(getSensorId(), BiometricOverlayConstants.REASON_AUTH_KEYGUARD, + this); try { mCancellationSignal = doDetectInteraction(); diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java index 612d90670888..7e5d39fdef1a 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java @@ -30,6 +30,7 @@ import android.hardware.fingerprint.Fingerprint; import android.hardware.fingerprint.FingerprintManager; import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; import android.hardware.fingerprint.ISidefpsController; +import android.hardware.fingerprint.IUdfpsOverlay; import android.hardware.fingerprint.IUdfpsOverlayController; import android.hardware.keymaster.HardwareAuthToken; import android.os.IBinder; @@ -86,6 +87,7 @@ class FingerprintEnrollClient extends EnrollClient<AidlSession> implements Udfps @NonNull FingerprintSensorPropertiesInternal sensorProps, @Nullable IUdfpsOverlayController udfpsOverlayController, @Nullable ISidefpsController sidefpsController, + @Nullable IUdfpsOverlay udfpsOverlay, int maxTemplatesPerUser, @FingerprintManager.EnrollReason int enrollReason) { // UDFPS haptics occur when an image is acquired (instead of when the result is known) super(context, lazyDaemon, token, listener, userId, hardwareAuthToken, owner, utils, @@ -93,7 +95,8 @@ class FingerprintEnrollClient extends EnrollClient<AidlSession> implements Udfps biometricContext); setRequestId(requestId); mSensorProps = sensorProps; - mSensorOverlays = new SensorOverlays(udfpsOverlayController, sidefpsController); + mSensorOverlays = new SensorOverlays(udfpsOverlayController, + sidefpsController, udfpsOverlay); mMaxTemplatesPerUser = maxTemplatesPerUser; mALSProbeCallback = getLogger().getAmbientLightProbe(true /* startWithClient */); @@ -162,7 +165,8 @@ class FingerprintEnrollClient extends EnrollClient<AidlSession> implements Udfps @Override protected void startHalOperation() { - mSensorOverlays.show(getSensorId(), getOverlayReasonFromEnrollReason(mEnrollReason), this); + mSensorOverlays.show(getSensorId(), getOverlayReasonFromEnrollReason(mEnrollReason), + this); BiometricNotificationUtils.cancelBadCalibrationNotification(getContext()); try { diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java index 17ba07f2c2bd..a42ff9a8a2ba 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java @@ -40,6 +40,7 @@ import android.hardware.fingerprint.FingerprintManager; import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; import android.hardware.fingerprint.IFingerprintServiceReceiver; import android.hardware.fingerprint.ISidefpsController; +import android.hardware.fingerprint.IUdfpsOverlay; import android.hardware.fingerprint.IUdfpsOverlayController; import android.os.Binder; import android.os.Handler; @@ -108,6 +109,7 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi @Nullable private IFingerprint mDaemon; @Nullable private IUdfpsOverlayController mUdfpsOverlayController; @Nullable private ISidefpsController mSidefpsController; + @Nullable private IUdfpsOverlay mUdfpsOverlay; private final class BiometricTaskStackListener extends TaskStackListener { @Override @@ -383,29 +385,20 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi BiometricsProtoEnums.CLIENT_UNKNOWN), mBiometricContext, mSensors.get(sensorId).getSensorProperties(), - mUdfpsOverlayController, mSidefpsController, maxTemplatesPerUser, enrollReason); - scheduleForSensor(sensorId, client, new ClientMonitorCallback() { - - @Override - public void onClientStarted(@NonNull BaseClientMonitor clientMonitor) { - mBiometricStateCallback.onClientStarted(clientMonitor); - } - - @Override - public void onBiometricAction(int action) { - mBiometricStateCallback.onBiometricAction(action); - } - + mUdfpsOverlayController, mSidefpsController, mUdfpsOverlay, + maxTemplatesPerUser, enrollReason); + scheduleForSensor(sensorId, client, new ClientMonitorCompositeCallback( + mBiometricStateCallback, new ClientMonitorCallback() { @Override public void onClientFinished(@NonNull BaseClientMonitor clientMonitor, boolean success) { - mBiometricStateCallback.onClientFinished(clientMonitor, success); + ClientMonitorCallback.super.onClientFinished(clientMonitor, success); if (success) { scheduleLoadAuthenticatorIdsForUser(sensorId, userId); scheduleInvalidationRequest(sensorId, userId); } } - }); + })); }); return id; } @@ -428,7 +421,7 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi opPackageName, sensorId, createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient), mBiometricContext, - mUdfpsOverlayController, isStrongBiometric); + mUdfpsOverlayController, mUdfpsOverlay, isStrongBiometric); scheduleForSensor(sensorId, client, mBiometricStateCallback); }); @@ -449,10 +442,10 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient), mBiometricContext, isStrongBiometric, mTaskStackListener, mSensors.get(sensorId).getLockoutCache(), - mUdfpsOverlayController, mSidefpsController, allowBackgroundAuthentication, + mUdfpsOverlayController, mSidefpsController, mUdfpsOverlay, + allowBackgroundAuthentication, mSensors.get(sensorId).getSensorProperties(), mHandler, - Utils.getCurrentStrength(sensorId), - SystemClock.elapsedRealtimeClock()); + Utils.getCurrentStrength(sensorId), SystemClock.elapsedRealtimeClock()); scheduleForSensor(sensorId, client, mBiometricStateCallback); }); } @@ -659,6 +652,11 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi } @Override + public void setUdfpsOverlay(@NonNull IUdfpsOverlay controller) { + mUdfpsOverlay = controller; + } + + @Override public void dumpProtoState(int sensorId, @NonNull ProtoOutputStream proto, boolean clearSchedulerBuffer) { if (mSensors.contains(sensorId)) { diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java index 0e6df8e0df77..dbc96df9c458 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java @@ -38,6 +38,7 @@ import android.hardware.fingerprint.FingerprintSensorProperties; import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; import android.hardware.fingerprint.IFingerprintServiceReceiver; import android.hardware.fingerprint.ISidefpsController; +import android.hardware.fingerprint.IUdfpsOverlay; import android.hardware.fingerprint.IUdfpsOverlayController; import android.os.Handler; import android.os.IBinder; @@ -120,6 +121,7 @@ public class Fingerprint21 implements IHwBinder.DeathRecipient, ServiceProvider @NonNull private final HalResultController mHalResultController; @Nullable private IUdfpsOverlayController mUdfpsOverlayController; @Nullable private ISidefpsController mSidefpsController; + @Nullable private IUdfpsOverlay mUdfpsOverlay; @NonNull private final BiometricContext mBiometricContext; // for requests that do not use biometric prompt @NonNull private final AtomicLong mRequestCounter = new AtomicLong(0); @@ -594,7 +596,7 @@ public class Fingerprint21 implements IHwBinder.DeathRecipient, ServiceProvider createLogger(BiometricsProtoEnums.ACTION_ENROLL, BiometricsProtoEnums.CLIENT_UNKNOWN), mBiometricContext, - mUdfpsOverlayController, mSidefpsController, + mUdfpsOverlayController, mSidefpsController, mUdfpsOverlay, enrollReason); mScheduler.scheduleClientMonitor(client, new ClientMonitorCallback() { @Override @@ -640,7 +642,7 @@ public class Fingerprint21 implements IHwBinder.DeathRecipient, ServiceProvider mLazyDaemon, token, id, listener, userId, opPackageName, mSensorProperties.sensorId, createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient), - mBiometricContext, mUdfpsOverlayController, + mBiometricContext, mUdfpsOverlayController, mUdfpsOverlay, isStrongBiometric); mScheduler.scheduleClientMonitor(client, mBiometricStateCallback); }); @@ -664,7 +666,7 @@ public class Fingerprint21 implements IHwBinder.DeathRecipient, ServiceProvider createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient), mBiometricContext, isStrongBiometric, mTaskStackListener, mLockoutTracker, - mUdfpsOverlayController, mSidefpsController, + mUdfpsOverlayController, mSidefpsController, mUdfpsOverlay, allowBackgroundAuthentication, mSensorProperties); mScheduler.scheduleClientMonitor(client, mBiometricStateCallback); }); @@ -853,6 +855,11 @@ public class Fingerprint21 implements IHwBinder.DeathRecipient, ServiceProvider } @Override + public void setUdfpsOverlay(@NonNull IUdfpsOverlay controller) { + mUdfpsOverlay = controller; + } + + @Override public void dumpProtoState(int sensorId, @NonNull ProtoOutputStream proto, boolean clearSchedulerBuffer) { final long sensorToken = proto.start(SensorServiceStateProto.SENSOR_STATES); diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java index 0d620fd3a9e4..56fa36ec20e2 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java @@ -26,6 +26,7 @@ import android.hardware.biometrics.BiometricFingerprintConstants; import android.hardware.biometrics.fingerprint.V2_1.IBiometricsFingerprint; import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; import android.hardware.fingerprint.ISidefpsController; +import android.hardware.fingerprint.IUdfpsOverlay; import android.hardware.fingerprint.IUdfpsOverlayController; import android.os.IBinder; import android.os.RemoteException; @@ -76,15 +77,18 @@ class FingerprintAuthenticationClient extends AuthenticationClient<IBiometricsFi @NonNull LockoutFrameworkImpl lockoutTracker, @Nullable IUdfpsOverlayController udfpsOverlayController, @Nullable ISidefpsController sidefpsController, + @Nullable IUdfpsOverlay udfpsOverlay, boolean allowBackgroundAuthentication, @NonNull FingerprintSensorPropertiesInternal sensorProps) { super(context, lazyDaemon, token, listener, targetUserId, operationId, restricted, owner, cookie, requireConfirmation, sensorId, logger, biometricContext, - isStrongBiometric, taskStackListener, lockoutTracker, allowBackgroundAuthentication, - false /* shouldVibrate */, false /* isKeyguardBypassEnabled */); + isStrongBiometric, taskStackListener, lockoutTracker, + allowBackgroundAuthentication, false /* shouldVibrate */, + false /* isKeyguardBypassEnabled */); setRequestId(requestId); mLockoutFrameworkImpl = lockoutTracker; - mSensorOverlays = new SensorOverlays(udfpsOverlayController, sidefpsController); + mSensorOverlays = new SensorOverlays(udfpsOverlayController, + sidefpsController, udfpsOverlay); mSensorProps = sensorProps; mALSProbeCallback = getLogger().getAmbientLightProbe(false /* startWithClient */); } diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintDetectClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintDetectClient.java index c2929d0f15b2..3e9b8ef7fab4 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintDetectClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintDetectClient.java @@ -23,6 +23,7 @@ import android.hardware.biometrics.BiometricAuthenticator; import android.hardware.biometrics.BiometricFingerprintConstants; import android.hardware.biometrics.BiometricOverlayConstants; import android.hardware.biometrics.fingerprint.V2_1.IBiometricsFingerprint; +import android.hardware.fingerprint.IUdfpsOverlay; import android.hardware.fingerprint.IUdfpsOverlayController; import android.os.IBinder; import android.os.RemoteException; @@ -62,11 +63,13 @@ class FingerprintDetectClient extends AcquisitionClient<IBiometricsFingerprint> @NonNull ClientMonitorCallbackConverter listener, int userId, @NonNull String owner, int sensorId, @NonNull BiometricLogger biometricLogger, @NonNull BiometricContext biometricContext, - @Nullable IUdfpsOverlayController udfpsOverlayController, boolean isStrongBiometric) { + @Nullable IUdfpsOverlayController udfpsOverlayController, + @Nullable IUdfpsOverlay udfpsOverlay, boolean isStrongBiometric) { super(context, lazyDaemon, token, listener, userId, owner, 0 /* cookie */, sensorId, true /* shouldVibrate */, biometricLogger, biometricContext); setRequestId(requestId); - mSensorOverlays = new SensorOverlays(udfpsOverlayController, null /* sideFpsController */); + mSensorOverlays = new SensorOverlays(udfpsOverlayController, + null /* sideFpsController */, udfpsOverlay); mIsStrongBiometric = isStrongBiometric; } @@ -92,7 +95,8 @@ class FingerprintDetectClient extends AcquisitionClient<IBiometricsFingerprint> @Override protected void startHalOperation() { - mSensorOverlays.show(getSensorId(), BiometricOverlayConstants.REASON_AUTH_KEYGUARD, this); + mSensorOverlays.show(getSensorId(), BiometricOverlayConstants.REASON_AUTH_KEYGUARD, + this); try { getFreshDaemon().authenticate(0 /* operationId */, getTargetUserId()); @@ -128,8 +132,8 @@ class FingerprintDetectClient extends AcquisitionClient<IBiometricsFingerprint> } @Override - public void onAuthenticated(BiometricAuthenticator.Identifier identifier, boolean authenticated, - ArrayList<Byte> hardwareAuthToken) { + public void onAuthenticated(BiometricAuthenticator.Identifier identifier, + boolean authenticated, ArrayList<Byte> hardwareAuthToken) { getLogger().logOnAuthenticated(getContext(), getOperationContext(), authenticated, false /* requireConfirmation */, getTargetUserId(), false /* isBiometricPrompt */); diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintEnrollClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintEnrollClient.java index 5d9af5322c2e..3371cec32fde 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintEnrollClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintEnrollClient.java @@ -26,6 +26,7 @@ import android.hardware.biometrics.fingerprint.V2_1.IBiometricsFingerprint; import android.hardware.fingerprint.Fingerprint; import android.hardware.fingerprint.FingerprintManager; import android.hardware.fingerprint.ISidefpsController; +import android.hardware.fingerprint.IUdfpsOverlay; import android.hardware.fingerprint.IUdfpsOverlayController; import android.os.IBinder; import android.os.RemoteException; @@ -67,12 +68,14 @@ public class FingerprintEnrollClient extends EnrollClient<IBiometricsFingerprint @NonNull BiometricLogger biometricLogger, @NonNull BiometricContext biometricContext, @Nullable IUdfpsOverlayController udfpsOverlayController, @Nullable ISidefpsController sidefpsController, + @Nullable IUdfpsOverlay udfpsOverlay, @FingerprintManager.EnrollReason int enrollReason) { super(context, lazyDaemon, token, listener, userId, hardwareAuthToken, owner, utils, timeoutSec, sensorId, true /* shouldVibrate */, biometricLogger, biometricContext); setRequestId(requestId); - mSensorOverlays = new SensorOverlays(udfpsOverlayController, sidefpsController); + mSensorOverlays = new SensorOverlays(udfpsOverlayController, + sidefpsController, udfpsOverlay); mEnrollReason = enrollReason; if (enrollReason == FingerprintManager.ENROLL_FIND_SENSOR) { @@ -102,7 +105,8 @@ public class FingerprintEnrollClient extends EnrollClient<IBiometricsFingerprint @Override protected void startHalOperation() { - mSensorOverlays.show(getSensorId(), getOverlayReasonFromEnrollReason(mEnrollReason), this); + mSensorOverlays.show(getSensorId(), getOverlayReasonFromEnrollReason(mEnrollReason), + this); BiometricNotificationUtils.cancelBadCalibrationNotification(getContext()); try { diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java index 6795b6b4158d..45b0f0a6d04a 100644 --- a/services/core/java/com/android/server/connectivity/Vpn.java +++ b/services/core/java/com/android/server/connectivity/Vpn.java @@ -79,6 +79,7 @@ import android.net.NetworkScore; import android.net.RouteInfo; import android.net.UidRangeParcel; import android.net.UnderlyingNetworkInfo; +import android.net.Uri; import android.net.VpnManager; import android.net.VpnProfileState; import android.net.VpnService; @@ -226,6 +227,16 @@ public class Vpn { private static final int VPN_DEFAULT_SCORE = 101; /** + * The reset session timer for data stall. If a session has not successfully revalidated after + * the delay, the session will be torn down and restarted in an attempt to recover. Delay + * counter is reset on successful validation only. + * + * <p>If retries have exceeded the length of this array, the last entry in the array will be + * used as a repeating interval. + */ + private static final long[] DATA_STALL_RESET_DELAYS_SEC = {30L, 60L, 120L, 240L, 480L, 960L}; + + /** * The initial token value of IKE session. */ private static final int STARTING_TOKEN = -1; @@ -271,6 +282,7 @@ public class Vpn { private final UserManager mUserManager; private final VpnProfileStore mVpnProfileStore; + protected boolean mDataStallSuspected = false; @VisibleForTesting VpnProfileStore getVpnProfileStore() { @@ -522,12 +534,30 @@ public class Vpn { @NonNull LinkProperties lp, @NonNull NetworkScore score, @NonNull NetworkAgentConfig config, - @Nullable NetworkProvider provider) { + @Nullable NetworkProvider provider, + @Nullable ValidationStatusCallback callback) { return new VpnNetworkAgentWrapper( - context, looper, logTag, nc, lp, score, config, provider); + context, looper, logTag, nc, lp, score, config, provider, callback); + } + + /** + * Get the length of time to wait before resetting the ike session when a data stall is + * suspected. + */ + public long getDataStallResetSessionSeconds(int count) { + if (count >= DATA_STALL_RESET_DELAYS_SEC.length) { + return DATA_STALL_RESET_DELAYS_SEC[DATA_STALL_RESET_DELAYS_SEC.length - 1]; + } else { + return DATA_STALL_RESET_DELAYS_SEC[count]; + } } } + @VisibleForTesting + interface ValidationStatusCallback { + void onValidationStatus(int status); + } + public Vpn(Looper looper, Context context, INetworkManagementService netService, INetd netd, @UserIdInt int userId, VpnProfileStore vpnProfileStore) { this(looper, context, new Dependencies(), netService, netd, userId, vpnProfileStore, @@ -1460,6 +1490,11 @@ public class Vpn { @GuardedBy("this") private void agentConnect() { + agentConnect(null /* validationCallback */); + } + + @GuardedBy("this") + private void agentConnect(@Nullable ValidationStatusCallback validationCallback) { LinkProperties lp = makeLinkProperties(); // VPN either provide a default route (IPv4 or IPv6 or both), or they are a split tunnel @@ -1507,7 +1542,7 @@ public class Vpn { mNetworkAgent = mDeps.newNetworkAgent(mContext, mLooper, NETWORKTYPE /* logtag */, mNetworkCapabilities, lp, new NetworkScore.Builder().setLegacyInt(VPN_DEFAULT_SCORE).build(), - networkAgentConfig, mNetworkProvider); + networkAgentConfig, mNetworkProvider, validationCallback); final long token = Binder.clearCallingIdentity(); try { mNetworkAgent.register(); @@ -2723,7 +2758,7 @@ public class Vpn { @Nullable private ScheduledFuture<?> mScheduledHandleNetworkLostFuture; @Nullable private ScheduledFuture<?> mScheduledHandleRetryIkeSessionFuture; - + @Nullable private ScheduledFuture<?> mScheduledHandleDataStallFuture; /** Signal to ensure shutdown is honored even if a new Network is connected. */ private boolean mIsRunning = true; @@ -2750,6 +2785,14 @@ public class Vpn { private boolean mMobikeEnabled = false; /** + * The number of attempts to reset the IKE session since the last successful connection. + * + * <p>This variable controls the retry delay, and is reset when the VPN pass network + * validation. + */ + private int mDataStallRetryCount = 0; + + /** * The number of attempts since the last successful connection. * * <p>This variable controls the retry delay, and is reset when a new IKE session is @@ -2931,7 +2974,7 @@ public class Vpn { if (isSettingsVpnLocked()) { prepareStatusIntent(); } - agentConnect(); + agentConnect(this::onValidationStatus); return; // Link properties are already sent. } else { // Underlying networks also set in agentConnect() @@ -3200,18 +3243,52 @@ public class Vpn { // Ignore stale runner. if (mVpnRunner != Vpn.IkeV2VpnRunner.this) return; - // Handle the report only for current VPN network. + // Handle the report only for current VPN network. If data stall is already + // reported, ignoring the other reports. It means that the stall is not + // recovered by MOBIKE and should be on the way to reset the ike session. if (mNetworkAgent != null - && mNetworkAgent.getNetwork().equals(report.getNetwork())) { + && mNetworkAgent.getNetwork().equals(report.getNetwork()) + && !mDataStallSuspected) { Log.d(TAG, "Data stall suspected"); // Trigger MOBIKE. maybeMigrateIkeSession(mActiveNetwork); + mDataStallSuspected = true; } } } } + public void onValidationStatus(int status) { + if (status == NetworkAgent.VALIDATION_STATUS_VALID) { + // No data stall now. Reset it. + mExecutor.execute(() -> { + mDataStallSuspected = false; + mDataStallRetryCount = 0; + if (mScheduledHandleDataStallFuture != null) { + Log.d(TAG, "Recovered from stall. Cancel pending reset action."); + mScheduledHandleDataStallFuture.cancel(false /* mayInterruptIfRunning */); + mScheduledHandleDataStallFuture = null; + } + }); + } else { + // Skip other invalid status if the scheduled recovery exists. + if (mScheduledHandleDataStallFuture != null) return; + + mScheduledHandleDataStallFuture = mExecutor.schedule(() -> { + if (mDataStallSuspected) { + Log.d(TAG, "Reset session to recover stalled network"); + // This will reset old state if it exists. + startIkeSession(mActiveNetwork); + } + + // Reset mScheduledHandleDataStallFuture since it's already run on executor + // thread. + mScheduledHandleDataStallFuture = null; + }, mDeps.getDataStallResetSessionSeconds(mDataStallRetryCount++), TimeUnit.SECONDS); + } + } + /** * Handles loss of the default underlying network * @@ -4339,6 +4416,7 @@ public class Vpn { // un-finalized. @VisibleForTesting public static class VpnNetworkAgentWrapper extends NetworkAgent { + private final ValidationStatusCallback mCallback; /** Create an VpnNetworkAgentWrapper */ public VpnNetworkAgentWrapper( @NonNull Context context, @@ -4348,8 +4426,10 @@ public class Vpn { @NonNull LinkProperties lp, @NonNull NetworkScore score, @NonNull NetworkAgentConfig config, - @Nullable NetworkProvider provider) { + @Nullable NetworkProvider provider, + @Nullable ValidationStatusCallback callback) { super(context, looper, logTag, nc, lp, score, config, provider); + mCallback = callback; } /** Update the LinkProperties */ @@ -4371,6 +4451,13 @@ public class Vpn { public void onNetworkUnwanted() { // We are user controlled, not driven by NetworkRequest. } + + @Override + public void onValidationStatus(int status, Uri redirectUri) { + if (mCallback != null) { + mCallback.onValidationStatus(status); + } + } } /** diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index 7e80b7d5b0ac..e907ebfa6471 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -127,6 +127,7 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.display.BrightnessSynchronizer; import com.android.internal.util.DumpUtils; +import com.android.internal.util.FrameworkStatsLog; import com.android.internal.util.IndentingPrintWriter; import com.android.server.AnimationThread; import com.android.server.DisplayThread; @@ -151,6 +152,7 @@ import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Consumer; + /** * Manages attached displays. * <p> @@ -1900,6 +1902,14 @@ public final class DisplayManagerService extends SystemService { if (displayDevice == null) { return; } + if (mLogicalDisplayMapper.getDisplayLocked(displayDevice) + .getDisplayInfoLocked().type == Display.TYPE_INTERNAL) { + FrameworkStatsLog.write(FrameworkStatsLog.BRIGHTNESS_CONFIGURATION_UPDATED, + c.getCurve().first, + c.getCurve().second, + // should not be logged for virtual displays + uniqueId); + } mPersistentDataStore.setBrightnessConfigurationForDisplayLocked(c, displayDevice, userSerial, packageName); } finally { diff --git a/services/core/java/com/android/server/dreams/DreamController.java b/services/core/java/com/android/server/dreams/DreamController.java index cd9ef0915741..c3313e0d11ca 100644 --- a/services/core/java/com/android/server/dreams/DreamController.java +++ b/services/core/java/com/android/server/dreams/DreamController.java @@ -245,6 +245,7 @@ final class DreamController { if (mSentStartBroadcast) { mContext.sendBroadcastAsUser(mDreamingStoppedIntent, UserHandle.ALL); + mSentStartBroadcast = false; } mActivityTaskManager.removeRootTasksWithActivityTypes( diff --git a/services/core/java/com/android/server/dreams/DreamManagerService.java b/services/core/java/com/android/server/dreams/DreamManagerService.java index 6e2ccebb6ff4..4ca4817cc4fe 100644 --- a/services/core/java/com/android/server/dreams/DreamManagerService.java +++ b/services/core/java/com/android/server/dreams/DreamManagerService.java @@ -23,12 +23,14 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static com.android.server.wm.ActivityInterceptorCallback.DREAM_MANAGER_ORDERED_ID; +import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager; import android.app.TaskInfo; import android.content.BroadcastReceiver; import android.content.ComponentName; +import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; @@ -38,6 +40,8 @@ import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ServiceInfo; import android.database.ContentObserver; import android.hardware.display.AmbientDisplayConfiguration; +import android.net.Uri; +import android.os.BatteryManager; import android.os.Binder; import android.os.Build; import android.os.Handler; @@ -72,6 +76,8 @@ import com.android.server.wm.ActivityTaskManagerInternal; import java.io.FileDescriptor; import java.io.PrintWriter; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -88,6 +94,15 @@ public final class DreamManagerService extends SystemService { private static final String DOZE_WAKE_LOCK_TAG = "dream:doze"; private static final String DREAM_WAKE_LOCK_TAG = "dream:dream"; + /** Constants for the when to activate dreams. */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({DREAM_ON_DOCK, DREAM_ON_CHARGE, DREAM_ON_DOCK_OR_CHARGE}) + public @interface WhenToDream {} + private static final int DREAM_DISABLED = 0x0; + private static final int DREAM_ON_DOCK = 0x1; + private static final int DREAM_ON_CHARGE = 0x2; + private static final int DREAM_ON_DOCK_OR_CHARGE = 0x3; + private final Object mLock = new Object(); private final Context mContext; @@ -101,12 +116,20 @@ public final class DreamManagerService extends SystemService { private final DreamUiEventLogger mDreamUiEventLogger; private final ComponentName mAmbientDisplayComponent; private final boolean mDismissDreamOnActivityStart; + private final boolean mDreamsOnlyEnabledForSystemUser; + private final boolean mDreamsEnabledByDefaultConfig; + private final boolean mDreamsActivatedOnChargeByDefault; + private final boolean mDreamsActivatedOnDockByDefault; @GuardedBy("mLock") private DreamRecord mCurrentDream; private boolean mForceAmbientDisplayEnabled; - private final boolean mDreamsOnlyEnabledForSystemUser; + private SettingsObserver mSettingsObserver; + private boolean mDreamsEnabledSetting; + @WhenToDream private int mWhenToDream; + private boolean mIsDocked; + private boolean mIsCharging; // A temporary dream component that, when present, takes precedence over user configured dream // component. @@ -144,6 +167,37 @@ public final class DreamManagerService extends SystemService { } }; + private final BroadcastReceiver mChargingReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + mIsCharging = (BatteryManager.ACTION_CHARGING.equals(intent.getAction())); + } + }; + + private final BroadcastReceiver mDockStateReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (Intent.ACTION_DOCK_EVENT.equals(intent.getAction())) { + int dockState = intent.getIntExtra(Intent.EXTRA_DOCK_STATE, + Intent.EXTRA_DOCK_STATE_UNDOCKED); + mIsDocked = dockState != Intent.EXTRA_DOCK_STATE_UNDOCKED; + } + } + }; + + private final class SettingsObserver extends ContentObserver { + SettingsObserver(Handler handler) { + super(handler); + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + synchronized (mLock) { + updateWhenToDreamSettings(); + } + } + } + public DreamManagerService(Context context) { super(context); mContext = context; @@ -164,6 +218,14 @@ public final class DreamManagerService extends SystemService { mContext.getResources().getBoolean(R.bool.config_dreamsOnlyEnabledForSystemUser); mDismissDreamOnActivityStart = mContext.getResources().getBoolean( R.bool.config_dismissDreamOnActivityStart); + + mDreamsEnabledByDefaultConfig = mContext.getResources().getBoolean( + com.android.internal.R.bool.config_dreamsEnabledByDefault); + mDreamsActivatedOnChargeByDefault = mContext.getResources().getBoolean( + com.android.internal.R.bool.config_dreamsActivatedOnSleepByDefault); + mDreamsActivatedOnDockByDefault = mContext.getResources().getBoolean( + com.android.internal.R.bool.config_dreamsActivatedOnDockByDefault); + mSettingsObserver = new SettingsObserver(mHandler); } @Override @@ -197,6 +259,30 @@ public final class DreamManagerService extends SystemService { DREAM_MANAGER_ORDERED_ID, mActivityInterceptorCallback); } + + mContext.registerReceiver( + mDockStateReceiver, new IntentFilter(Intent.ACTION_DOCK_EVENT)); + IntentFilter chargingIntentFilter = new IntentFilter(); + chargingIntentFilter.addAction(BatteryManager.ACTION_CHARGING); + chargingIntentFilter.addAction(BatteryManager.ACTION_DISCHARGING); + mContext.registerReceiver(mChargingReceiver, chargingIntentFilter); + + mSettingsObserver = new SettingsObserver(mHandler); + mContext.getContentResolver().registerContentObserver(Settings.Secure.getUriFor( + Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP), + false, mSettingsObserver, UserHandle.USER_ALL); + mContext.getContentResolver().registerContentObserver(Settings.Secure.getUriFor( + Settings.Secure.SCREENSAVER_ACTIVATE_ON_DOCK), + false, mSettingsObserver, UserHandle.USER_ALL); + mContext.getContentResolver().registerContentObserver(Settings.Secure.getUriFor( + Settings.Secure.SCREENSAVER_ENABLED), + false, mSettingsObserver, UserHandle.USER_ALL); + + // We don't get an initial broadcast for the batter state, so we have to initialize + // directly from BatteryManager. + mIsCharging = mContext.getSystemService(BatteryManager.class).isCharging(); + + updateWhenToDreamSettings(); } } @@ -207,6 +293,14 @@ public final class DreamManagerService extends SystemService { pw.println("mCurrentDream=" + mCurrentDream); pw.println("mForceAmbientDisplayEnabled=" + mForceAmbientDisplayEnabled); pw.println("mDreamsOnlyEnabledForSystemUser=" + mDreamsOnlyEnabledForSystemUser); + pw.println("mDreamsEnabledSetting=" + mDreamsEnabledSetting); + pw.println("mForceAmbientDisplayEnabled=" + mForceAmbientDisplayEnabled); + pw.println("mDreamsOnlyEnabledForSystemUser=" + mDreamsOnlyEnabledForSystemUser); + pw.println("mDreamsActivatedOnDockByDefault=" + mDreamsActivatedOnDockByDefault); + pw.println("mDreamsActivatedOnChargeByDefault=" + mDreamsActivatedOnChargeByDefault); + pw.println("mIsDocked=" + mIsDocked); + pw.println("mIsCharging=" + mIsCharging); + pw.println("mWhenToDream=" + mWhenToDream); pw.println("getDozeComponent()=" + getDozeComponent()); pw.println(); @@ -214,7 +308,28 @@ public final class DreamManagerService extends SystemService { } } - /** Whether a real dream is occurring. */ + private void updateWhenToDreamSettings() { + synchronized (mLock) { + final ContentResolver resolver = mContext.getContentResolver(); + + final int activateWhenCharging = (Settings.Secure.getIntForUser(resolver, + Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, + mDreamsActivatedOnChargeByDefault ? 1 : 0, + UserHandle.USER_CURRENT) != 0) ? DREAM_ON_CHARGE : DREAM_DISABLED; + final int activateWhenDocked = (Settings.Secure.getIntForUser(resolver, + Settings.Secure.SCREENSAVER_ACTIVATE_ON_DOCK, + mDreamsActivatedOnDockByDefault ? 1 : 0, + UserHandle.USER_CURRENT) != 0) ? DREAM_ON_DOCK : DREAM_DISABLED; + mWhenToDream = activateWhenCharging + activateWhenDocked; + + mDreamsEnabledSetting = (Settings.Secure.getIntForUser(resolver, + Settings.Secure.SCREENSAVER_ENABLED, + mDreamsEnabledByDefaultConfig ? 1 : 0, + UserHandle.USER_CURRENT) != 0); + } + } + + /** Whether a real dream is occurring. */ private boolean isDreamingInternal() { synchronized (mLock) { return mCurrentDream != null && !mCurrentDream.isPreview @@ -236,6 +351,30 @@ public final class DreamManagerService extends SystemService { } } + /** Whether dreaming can start given user settings and the current dock/charge state. */ + private boolean canStartDreamingInternal(boolean isScreenOn) { + synchronized (mLock) { + // Can't start dreaming if we are already dreaming. + if (isScreenOn && isDreamingInternal()) { + return false; + } + + if (!mDreamsEnabledSetting) { + return false; + } + + if ((mWhenToDream & DREAM_ON_CHARGE) == DREAM_ON_CHARGE) { + return mIsCharging; + } + + if ((mWhenToDream & DREAM_ON_DOCK) == DREAM_ON_DOCK) { + return mIsDocked; + } + + return false; + } + } + protected void requestStartDreamFromShell() { requestDreamInternal(); } @@ -869,6 +1008,11 @@ public final class DreamManagerService extends SystemService { } @Override + public boolean canStartDreaming(boolean isScreenOn) { + return canStartDreamingInternal(isScreenOn); + } + + @Override public ComponentName getActiveDreamComponent(boolean doze) { return getActiveDreamComponentInternal(doze); } diff --git a/services/core/java/com/android/server/infra/FrameworkResourcesServiceNameResolver.java b/services/core/java/com/android/server/infra/FrameworkResourcesServiceNameResolver.java index 5253d34a38f0..d4e8f27a7b34 100644 --- a/services/core/java/com/android/server/infra/FrameworkResourcesServiceNameResolver.java +++ b/services/core/java/com/android/server/infra/FrameworkResourcesServiceNameResolver.java @@ -19,28 +19,9 @@ import android.annotation.ArrayRes; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.StringRes; -import android.annotation.UserIdInt; -import android.app.AppGlobals; -import android.content.ComponentName; import android.content.Context; -import android.content.pm.PackageManager; -import android.content.pm.ServiceInfo; -import android.os.Handler; -import android.os.Looper; -import android.os.Message; -import android.os.SystemClock; -import android.text.TextUtils; -import android.util.Slog; -import android.util.SparseArray; -import android.util.SparseBooleanArray; -import android.util.TimeUtils; - -import com.android.internal.annotations.GuardedBy; import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; /** * Gets the service name using a framework resources, temporarily changing the service if necessary @@ -48,259 +29,42 @@ import java.util.List; * * @hide */ -public final class FrameworkResourcesServiceNameResolver implements ServiceNameResolver { - - private static final String TAG = FrameworkResourcesServiceNameResolver.class.getSimpleName(); - - /** Handler message to {@link #resetTemporaryService(int)} */ - private static final int MSG_RESET_TEMPORARY_SERVICE = 0; +public final class FrameworkResourcesServiceNameResolver extends ServiceNameBaseResolver { - @NonNull - private final Context mContext; - @NonNull - private final Object mLock = new Object(); - @StringRes private final int mStringResourceId; @ArrayRes private final int mArrayResourceId; - private final boolean mIsMultiple; - /** - * Map of temporary service name list set by {@link #setTemporaryServices(int, String[], int)}, - * keyed by {@code userId}. - * - * <p>Typically used by Shell command and/or CTS tests to configure temporary services if - * mIsMultiple is true. - */ - @GuardedBy("mLock") - private final SparseArray<String[]> mTemporaryServiceNamesList = new SparseArray<>(); - /** - * Map of default services that have been disabled by - * {@link #setDefaultServiceEnabled(int, boolean)},keyed by {@code userId}. - * - * <p>Typically used by Shell command and/or CTS tests. - */ - @GuardedBy("mLock") - private final SparseBooleanArray mDefaultServicesDisabled = new SparseBooleanArray(); - @Nullable - private NameResolverListener mOnSetCallback; - /** - * When the temporary service will expire (and reset back to the default). - */ - @GuardedBy("mLock") - private long mTemporaryServiceExpiration; - - /** - * Handler used to reset the temporary service name. - */ - @GuardedBy("mLock") - private Handler mTemporaryHandler; public FrameworkResourcesServiceNameResolver(@NonNull Context context, @StringRes int resourceId) { - mContext = context; + super(context, false); mStringResourceId = resourceId; mArrayResourceId = -1; - mIsMultiple = false; } public FrameworkResourcesServiceNameResolver(@NonNull Context context, @ArrayRes int resourceId, boolean isMultiple) { + super(context, isMultiple); if (!isMultiple) { throw new UnsupportedOperationException("Please use " + "FrameworkResourcesServiceNameResolver(context, @StringRes int) constructor " + "if single service mode is requested."); } - mContext = context; mStringResourceId = -1; mArrayResourceId = resourceId; - mIsMultiple = true; - } - - @Override - public void setOnTemporaryServiceNameChangedCallback(@NonNull NameResolverListener callback) { - synchronized (mLock) { - this.mOnSetCallback = callback; - } - } - - @Override - public String getServiceName(@UserIdInt int userId) { - String[] serviceNames = getServiceNameList(userId); - return (serviceNames == null || serviceNames.length == 0) ? null : serviceNames[0]; - } - - @Override - public String getDefaultServiceName(@UserIdInt int userId) { - String[] serviceNames = getDefaultServiceNameList(userId); - return (serviceNames == null || serviceNames.length == 0) ? null : serviceNames[0]; - } - - /** - * Gets the default list of the service names for the given user. - * - * <p>Typically implemented by services which want to provide multiple backends. - */ - @Override - public String[] getServiceNameList(int userId) { - synchronized (mLock) { - String[] temporaryNames = mTemporaryServiceNamesList.get(userId); - if (temporaryNames != null) { - // Always log it, as it should only be used on CTS or during development - Slog.w(TAG, "getServiceName(): using temporary name " - + Arrays.toString(temporaryNames) + " for user " + userId); - return temporaryNames; - } - final boolean disabled = mDefaultServicesDisabled.get(userId); - if (disabled) { - // Always log it, as it should only be used on CTS or during development - Slog.w(TAG, "getServiceName(): temporary name not set and default disabled for " - + "user " + userId); - return null; - } - return getDefaultServiceNameList(userId); - - } - } - - /** - * Gets the default list of the service names for the given user. - * - * <p>Typically implemented by services which want to provide multiple backends. - */ - @Override - public String[] getDefaultServiceNameList(int userId) { - synchronized (mLock) { - if (mIsMultiple) { - String[] serviceNameList = mContext.getResources().getStringArray(mArrayResourceId); - // Filter out unimplemented services - // Initialize the validated array as null because we do not know the final size. - List<String> validatedServiceNameList = new ArrayList<>(); - try { - for (int i = 0; i < serviceNameList.length; i++) { - if (TextUtils.isEmpty(serviceNameList[i])) { - continue; - } - ComponentName serviceComponent = ComponentName.unflattenFromString( - serviceNameList[i]); - ServiceInfo serviceInfo = AppGlobals.getPackageManager().getServiceInfo( - serviceComponent, - PackageManager.MATCH_DIRECT_BOOT_AWARE - | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, userId); - if (serviceInfo != null) { - validatedServiceNameList.add(serviceNameList[i]); - } - } - } catch (Exception e) { - Slog.e(TAG, "Could not validate provided services.", e); - } - String[] validatedServiceNameArray = new String[validatedServiceNameList.size()]; - return validatedServiceNameList.toArray(validatedServiceNameArray); - } else { - final String name = mContext.getString(mStringResourceId); - return TextUtils.isEmpty(name) ? new String[0] : new String[]{name}; - } - } - } - - @Override - public boolean isConfiguredInMultipleMode() { - return mIsMultiple; - } - - @Override - public boolean isTemporary(@UserIdInt int userId) { - synchronized (mLock) { - return mTemporaryServiceNamesList.get(userId) != null; - } } @Override - public void setTemporaryService(@UserIdInt int userId, @NonNull String componentName, - int durationMs) { - setTemporaryServices(userId, new String[]{componentName}, durationMs); - } - - @Override - public void setTemporaryServices(int userId, @NonNull String[] componentNames, int durationMs) { - synchronized (mLock) { - mTemporaryServiceNamesList.put(userId, componentNames); - - if (mTemporaryHandler == null) { - mTemporaryHandler = new Handler(Looper.getMainLooper(), null, true) { - @Override - public void handleMessage(Message msg) { - if (msg.what == MSG_RESET_TEMPORARY_SERVICE) { - synchronized (mLock) { - resetTemporaryService(userId); - } - } else { - Slog.wtf(TAG, "invalid handler msg: " + msg); - } - } - }; - } else { - mTemporaryHandler.removeMessages(MSG_RESET_TEMPORARY_SERVICE); - } - mTemporaryServiceExpiration = SystemClock.elapsedRealtime() + durationMs; - mTemporaryHandler.sendEmptyMessageDelayed(MSG_RESET_TEMPORARY_SERVICE, durationMs); - for (int i = 0; i < componentNames.length; i++) { - notifyTemporaryServiceNameChangedLocked(userId, componentNames[i], - /* isTemporary= */ true); - } - } - } - - @Override - public void resetTemporaryService(@UserIdInt int userId) { - synchronized (mLock) { - Slog.i(TAG, "resetting temporary service for user " + userId + " from " - + Arrays.toString(mTemporaryServiceNamesList.get(userId))); - mTemporaryServiceNamesList.remove(userId); - if (mTemporaryHandler != null) { - mTemporaryHandler.removeMessages(MSG_RESET_TEMPORARY_SERVICE); - mTemporaryHandler = null; - } - notifyTemporaryServiceNameChangedLocked(userId, /* newTemporaryName= */ null, - /* isTemporary= */ false); - } - } - - @Override - public boolean setDefaultServiceEnabled(int userId, boolean enabled) { - synchronized (mLock) { - final boolean currentlyEnabled = isDefaultServiceEnabledLocked(userId); - if (currentlyEnabled == enabled) { - Slog.i(TAG, "setDefaultServiceEnabled(" + userId + "): already " + enabled); - return false; - } - if (enabled) { - Slog.i(TAG, "disabling default service for user " + userId); - mDefaultServicesDisabled.removeAt(userId); - } else { - Slog.i(TAG, "enabling default service for user " + userId); - mDefaultServicesDisabled.put(userId, true); - } - } - return true; + public String[] readServiceNameList(int userId) { + return mContext.getResources().getStringArray(mArrayResourceId); } + @Nullable @Override - public boolean isDefaultServiceEnabled(int userId) { - synchronized (mLock) { - return isDefaultServiceEnabledLocked(userId); - } + public String readServiceName(int userId) { + return mContext.getResources().getString(mStringResourceId); } - private boolean isDefaultServiceEnabledLocked(int userId) { - return !mDefaultServicesDisabled.get(userId); - } - - @Override - public String toString() { - synchronized (mLock) { - return "FrameworkResourcesServiceNamer[temps=" + mTemporaryServiceNamesList + "]"; - } - } // TODO(b/117779333): support proto @Override @@ -314,31 +78,4 @@ public final class FrameworkResourcesServiceNameResolver implements ServiceNameR pw.print(mDefaultServicesDisabled.size()); } } - - // TODO(b/117779333): support proto - @Override - public void dumpShort(@NonNull PrintWriter pw, @UserIdInt int userId) { - synchronized (mLock) { - final String[] temporaryNames = mTemporaryServiceNamesList.get(userId); - if (temporaryNames != null) { - pw.print("tmpName="); - pw.print(Arrays.toString(temporaryNames)); - final long ttl = mTemporaryServiceExpiration - SystemClock.elapsedRealtime(); - pw.print(" (expires in "); - TimeUtils.formatDuration(ttl, pw); - pw.print("), "); - } - pw.print("defaultName="); - pw.print(getDefaultServiceName(userId)); - final boolean disabled = mDefaultServicesDisabled.get(userId); - pw.println(disabled ? " (disabled)" : " (enabled)"); - } - } - - private void notifyTemporaryServiceNameChangedLocked(@UserIdInt int userId, - @Nullable String newTemporaryName, boolean isTemporary) { - if (mOnSetCallback != null) { - mOnSetCallback.onNameResolved(userId, newTemporaryName, isTemporary); - } - } } diff --git a/services/core/java/com/android/server/infra/SecureSettingsServiceNameResolver.java b/services/core/java/com/android/server/infra/SecureSettingsServiceNameResolver.java index cac7f53aad66..17d75e600c36 100644 --- a/services/core/java/com/android/server/infra/SecureSettingsServiceNameResolver.java +++ b/services/core/java/com/android/server/infra/SecureSettingsServiceNameResolver.java @@ -19,8 +19,11 @@ import android.annotation.NonNull; import android.annotation.UserIdInt; import android.content.Context; import android.provider.Settings; +import android.text.TextUtils; +import android.util.ArraySet; import java.io.PrintWriter; +import java.util.Set; /** * Gets the service name using a property from the {@link android.provider.Settings.Secure} @@ -28,21 +31,34 @@ import java.io.PrintWriter; * * @hide */ -public final class SecureSettingsServiceNameResolver implements ServiceNameResolver { +public final class SecureSettingsServiceNameResolver extends ServiceNameBaseResolver { + /** + * The delimiter to be used to parse the secure settings string. Services must make sure + * that this delimiter is used while adding component names to their secure setting property. + */ + private static final char COMPONENT_NAME_SEPARATOR = ':'; - private final @NonNull Context mContext; + private final TextUtils.SimpleStringSplitter mStringColonSplitter = + new TextUtils.SimpleStringSplitter(COMPONENT_NAME_SEPARATOR); @NonNull private final String mProperty; public SecureSettingsServiceNameResolver(@NonNull Context context, @NonNull String property) { - mContext = context; - mProperty = property; + this(context, property, /*isMultiple*/false); } - @Override - public String getDefaultServiceName(@UserIdInt int userId) { - return Settings.Secure.getStringForUser(mContext.getContentResolver(), mProperty, userId); + /** + * + * @param context the context required to retrieve the secure setting value + * @param property name of the secure setting key + * @param isMultiple true if the system service using this resolver needs to connect to + * multiple remote services, false otherwise + */ + public SecureSettingsServiceNameResolver(@NonNull Context context, @NonNull String property, + boolean isMultiple) { + super(context, isMultiple); + mProperty = property; } // TODO(b/117779333): support proto @@ -61,4 +77,34 @@ public final class SecureSettingsServiceNameResolver implements ServiceNameResol public String toString() { return "SecureSettingsServiceNameResolver[" + mProperty + "]"; } + + @Override + public String[] readServiceNameList(int userId) { + return parseColonDelimitedServiceNames( + Settings.Secure.getStringForUser( + mContext.getContentResolver(), mProperty, userId)); + } + + @Override + public String readServiceName(int userId) { + return Settings.Secure.getStringForUser( + mContext.getContentResolver(), mProperty, userId); + } + + private String[] parseColonDelimitedServiceNames(String serviceNames) { + final Set<String> delimitedServices = new ArraySet<>(); + if (!TextUtils.isEmpty(serviceNames)) { + final TextUtils.SimpleStringSplitter splitter = mStringColonSplitter; + splitter.setString(serviceNames); + while (splitter.hasNext()) { + final String str = splitter.next(); + if (TextUtils.isEmpty(str)) { + continue; + } + delimitedServices.add(str); + } + } + String[] delimitedServicesArray = new String[delimitedServices.size()]; + return delimitedServices.toArray(delimitedServicesArray); + } } diff --git a/services/core/java/com/android/server/infra/ServiceNameBaseResolver.java b/services/core/java/com/android/server/infra/ServiceNameBaseResolver.java new file mode 100644 index 000000000000..76ea05e36141 --- /dev/null +++ b/services/core/java/com/android/server/infra/ServiceNameBaseResolver.java @@ -0,0 +1,325 @@ +/* + * Copyright (C) 2018 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.infra; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.UserIdInt; +import android.app.AppGlobals; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.pm.ServiceInfo; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.SystemClock; +import android.text.TextUtils; +import android.util.Slog; +import android.util.SparseArray; +import android.util.SparseBooleanArray; +import android.util.TimeUtils; + +import com.android.internal.annotations.GuardedBy; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Gets the service name using a framework resources, temporarily changing the service if necessary + * (typically during CTS tests or service development). + * + * @hide + */ +public abstract class ServiceNameBaseResolver implements ServiceNameResolver { + + private static final String TAG = ServiceNameBaseResolver.class.getSimpleName(); + + /** Handler message to {@link #resetTemporaryService(int)} */ + private static final int MSG_RESET_TEMPORARY_SERVICE = 0; + + @NonNull + protected final Context mContext; + @NonNull + protected final Object mLock = new Object(); + + protected final boolean mIsMultiple; + /** + * Map of temporary service name list set by {@link #setTemporaryServices(int, String[], int)}, + * keyed by {@code userId}. + * + * <p>Typically used by Shell command and/or CTS tests to configure temporary services if + * mIsMultiple is true. + */ + @GuardedBy("mLock") + protected final SparseArray<String[]> mTemporaryServiceNamesList = new SparseArray<>(); + /** + * Map of default services that have been disabled by + * {@link #setDefaultServiceEnabled(int, boolean)},keyed by {@code userId}. + * + * <p>Typically used by Shell command and/or CTS tests. + */ + @GuardedBy("mLock") + protected final SparseBooleanArray mDefaultServicesDisabled = new SparseBooleanArray(); + @Nullable + private NameResolverListener mOnSetCallback; + /** + * When the temporary service will expire (and reset back to the default). + */ + @GuardedBy("mLock") + private long mTemporaryServiceExpiration; + + /** + * Handler used to reset the temporary service name. + */ + @GuardedBy("mLock") + private Handler mTemporaryHandler; + + protected ServiceNameBaseResolver(Context context, boolean isMultiple) { + mContext = context; + mIsMultiple = isMultiple; + } + + @Override + public void setOnTemporaryServiceNameChangedCallback(@NonNull NameResolverListener callback) { + synchronized (mLock) { + this.mOnSetCallback = callback; + } + } + + @Override + public String getServiceName(@UserIdInt int userId) { + String[] serviceNames = getServiceNameList(userId); + return (serviceNames == null || serviceNames.length == 0) ? null : serviceNames[0]; + } + + @Override + public String getDefaultServiceName(@UserIdInt int userId) { + String[] serviceNames = getDefaultServiceNameList(userId); + return (serviceNames == null || serviceNames.length == 0) ? null : serviceNames[0]; + } + + /** + * Gets the default list of the service names for the given user. + * + * <p>Typically implemented by services which want to provide multiple backends. + */ + @Override + public String[] getServiceNameList(int userId) { + synchronized (mLock) { + String[] temporaryNames = mTemporaryServiceNamesList.get(userId); + if (temporaryNames != null) { + // Always log it, as it should only be used on CTS or during development + Slog.w(TAG, "getServiceName(): using temporary name " + + Arrays.toString(temporaryNames) + " for user " + userId); + return temporaryNames; + } + final boolean disabled = mDefaultServicesDisabled.get(userId); + if (disabled) { + // Always log it, as it should only be used on CTS or during development + Slog.w(TAG, "getServiceName(): temporary name not set and default disabled for " + + "user " + userId); + return null; + } + return getDefaultServiceNameList(userId); + + } + } + + /** + * Base classes must override this to read from the desired config e.g. framework resource, + * secure settings etc. + */ + @Nullable + public abstract String[] readServiceNameList(int userId); + + /** + * Base classes must override this to read from the desired config e.g. framework resource, + * secure settings etc. + */ + @Nullable + public abstract String readServiceName(int userId); + + /** + * Gets the default list of the service names for the given user. + * + * <p>Typically implemented by services which want to provide multiple backends. + */ + @Override + public String[] getDefaultServiceNameList(int userId) { + synchronized (mLock) { + if (mIsMultiple) { + String[] serviceNameList = readServiceNameList(userId); + // Filter out unimplemented services + // Initialize the validated array as null because we do not know the final size. + List<String> validatedServiceNameList = new ArrayList<>(); + try { + for (int i = 0; i < serviceNameList.length; i++) { + if (TextUtils.isEmpty(serviceNameList[i])) { + continue; + } + ComponentName serviceComponent = ComponentName.unflattenFromString( + serviceNameList[i]); + ServiceInfo serviceInfo = AppGlobals.getPackageManager().getServiceInfo( + serviceComponent, + PackageManager.MATCH_DIRECT_BOOT_AWARE + | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, userId); + if (serviceInfo != null) { + validatedServiceNameList.add(serviceNameList[i]); + } + } + } catch (Exception e) { + Slog.e(TAG, "Could not validate provided services.", e); + } + String[] validatedServiceNameArray = new String[validatedServiceNameList.size()]; + return validatedServiceNameList.toArray(validatedServiceNameArray); + } else { + final String name = readServiceName(userId); + return TextUtils.isEmpty(name) ? new String[0] : new String[]{name}; + } + } + } + + @Override + public boolean isConfiguredInMultipleMode() { + return mIsMultiple; + } + + @Override + public boolean isTemporary(@UserIdInt int userId) { + synchronized (mLock) { + return mTemporaryServiceNamesList.get(userId) != null; + } + } + + @Override + public void setTemporaryService(@UserIdInt int userId, @NonNull String componentName, + int durationMs) { + setTemporaryServices(userId, new String[]{componentName}, durationMs); + } + + @Override + public void setTemporaryServices(int userId, @NonNull String[] componentNames, int durationMs) { + synchronized (mLock) { + mTemporaryServiceNamesList.put(userId, componentNames); + + if (mTemporaryHandler == null) { + mTemporaryHandler = new Handler(Looper.getMainLooper(), null, true) { + @Override + public void handleMessage(Message msg) { + if (msg.what == MSG_RESET_TEMPORARY_SERVICE) { + synchronized (mLock) { + resetTemporaryService(userId); + } + } else { + Slog.wtf(TAG, "invalid handler msg: " + msg); + } + } + }; + } else { + mTemporaryHandler.removeMessages(MSG_RESET_TEMPORARY_SERVICE); + } + mTemporaryServiceExpiration = SystemClock.elapsedRealtime() + durationMs; + mTemporaryHandler.sendEmptyMessageDelayed(MSG_RESET_TEMPORARY_SERVICE, durationMs); + for (int i = 0; i < componentNames.length; i++) { + notifyTemporaryServiceNameChangedLocked(userId, componentNames[i], + /* isTemporary= */ true); + } + } + } + + @Override + public void resetTemporaryService(@UserIdInt int userId) { + synchronized (mLock) { + Slog.i(TAG, "resetting temporary service for user " + userId + " from " + + Arrays.toString(mTemporaryServiceNamesList.get(userId))); + mTemporaryServiceNamesList.remove(userId); + if (mTemporaryHandler != null) { + mTemporaryHandler.removeMessages(MSG_RESET_TEMPORARY_SERVICE); + mTemporaryHandler = null; + } + notifyTemporaryServiceNameChangedLocked(userId, /* newTemporaryName= */ null, + /* isTemporary= */ false); + } + } + + @Override + public boolean setDefaultServiceEnabled(int userId, boolean enabled) { + synchronized (mLock) { + final boolean currentlyEnabled = isDefaultServiceEnabledLocked(userId); + if (currentlyEnabled == enabled) { + Slog.i(TAG, "setDefaultServiceEnabled(" + userId + "): already " + enabled); + return false; + } + if (enabled) { + Slog.i(TAG, "disabling default service for user " + userId); + mDefaultServicesDisabled.removeAt(userId); + } else { + Slog.i(TAG, "enabling default service for user " + userId); + mDefaultServicesDisabled.put(userId, true); + } + } + return true; + } + + @Override + public boolean isDefaultServiceEnabled(int userId) { + synchronized (mLock) { + return isDefaultServiceEnabledLocked(userId); + } + } + + @GuardedBy("mLock") + private boolean isDefaultServiceEnabledLocked(int userId) { + return !mDefaultServicesDisabled.get(userId); + } + + @Override + public String toString() { + synchronized (mLock) { + return "FrameworkResourcesServiceNamer[temps=" + mTemporaryServiceNamesList + "]"; + } + } + + // TODO(b/117779333): support proto + @Override + public void dumpShort(@NonNull PrintWriter pw, @UserIdInt int userId) { + synchronized (mLock) { + final String[] temporaryNames = mTemporaryServiceNamesList.get(userId); + if (temporaryNames != null) { + pw.print("tmpName="); + pw.print(Arrays.toString(temporaryNames)); + final long ttl = mTemporaryServiceExpiration - SystemClock.elapsedRealtime(); + pw.print(" (expires in "); + TimeUtils.formatDuration(ttl, pw); + pw.print("), "); + } + pw.print("defaultName="); + pw.print(getDefaultServiceName(userId)); + final boolean disabled = mDefaultServicesDisabled.get(userId); + pw.println(disabled ? " (disabled)" : " (enabled)"); + } + } + + private void notifyTemporaryServiceNameChangedLocked(@UserIdInt int userId, + @Nullable String newTemporaryName, boolean isTemporary) { + if (mOnSetCallback != null) { + mOnSetCallback.onNameResolved(userId, newTemporaryName, isTemporary); + } + } +} diff --git a/services/core/java/com/android/server/input/BatteryController.java b/services/core/java/com/android/server/input/BatteryController.java index 36199debaa6e..9d4f18113555 100644 --- a/services/core/java/com/android/server/input/BatteryController.java +++ b/services/core/java/com/android/server/input/BatteryController.java @@ -371,6 +371,17 @@ final class BatteryController { } } + public void notifyStylusGestureStarted(int deviceId, long eventTime) { + synchronized (mLock) { + final DeviceMonitor monitor = mDeviceMonitors.get(deviceId); + if (monitor == null) { + return; + } + + monitor.onStylusGestureStarted(eventTime); + } + } + public void dump(PrintWriter pw, String prefix) { synchronized (mLock) { final String indent = prefix + " "; @@ -557,6 +568,8 @@ final class BatteryController { public void onTimeout(long eventTime) {} + public void onStylusGestureStarted(long eventTime) {} + // Returns the current battery state that can be used to notify listeners BatteryController. public State getBatteryStateForReporting() { return new State(mState); @@ -600,6 +613,22 @@ final class BatteryController { } @Override + public void onStylusGestureStarted(long eventTime) { + processChangesAndNotify(eventTime, (time) -> { + final boolean wasValid = mValidityTimeoutCallback != null; + if (!wasValid && mState.capacity == 0.f) { + // Handle a special case where the USI device reports a battery capacity of 0 + // at boot until the first battery update. To avoid wrongly sending out a + // battery capacity of 0 if we detect stylus presence before the capacity + // is first updated, do not validate the battery state when the state is not + // valid and the capacity is 0. + return; + } + markUsiBatteryValid(); + }); + } + + @Override public void onTimeout(long eventTime) { processChangesAndNotify(eventTime, (time) -> markUsiBatteryInvalid()); } diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index 69b0e65e38da..31f63d864f6c 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -19,6 +19,8 @@ package com.android.server.input; import static android.provider.DeviceConfig.NAMESPACE_INPUT_NATIVE_BOOT; import static android.view.KeyEvent.KEYCODE_UNKNOWN; +import android.Manifest; +import android.annotation.EnforcePermission; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManagerInternal; @@ -2671,6 +2673,12 @@ public class InputManagerService extends IInputManager.Stub mBatteryController.unregisterBatteryListener(deviceId, listener, Binder.getCallingPid()); } + @EnforcePermission(Manifest.permission.BLUETOOTH) + @Override + public String getInputDeviceBluetoothAddress(int deviceId) { + return mNative.getBluetoothAddress(deviceId); + } + @Override public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return; @@ -3052,6 +3060,12 @@ public class InputManagerService extends IInputManager.Stub com.android.internal.R.bool.config_perDisplayFocusEnabled); } + // Native callback. + @SuppressWarnings("unused") + private void notifyStylusGestureStarted(int deviceId, long eventTime) { + mBatteryController.notifyStylusGestureStarted(deviceId, eventTime); + } + /** * Flatten a map into a string list, with value positioned directly next to the * key. diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java index 63c0a88bf467..cfa7fb141be1 100644 --- a/services/core/java/com/android/server/input/NativeInputManagerService.java +++ b/services/core/java/com/android/server/input/NativeInputManagerService.java @@ -204,6 +204,9 @@ interface NativeInputManagerService { /** Set the displayId on which the mouse cursor should be shown. */ void setPointerDisplayId(int displayId); + /** Get the bluetooth address of an input device if known, otherwise return null. */ + String getBluetoothAddress(int deviceId); + /** The native implementation of InputManagerService methods. */ class NativeImpl implements NativeInputManagerService { /** Pointer to native input manager service object, used by native code. */ @@ -418,5 +421,8 @@ interface NativeInputManagerService { @Override public native void setPointerDisplayId(int displayId); + + @Override + public native String getBluetoothAddress(int deviceId); } } diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 76495b17c984..9cb8f43c452c 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -4601,12 +4601,6 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } if (!setVisible) { if (mCurClient != null) { - // IMMS only knows of focused window, not the actual IME target. - // e.g. it isn't aware of any window that has both - // NOT_FOCUSABLE, ALT_FOCUSABLE_IM flags set and can the IME target. - // Send it to window manager to hide IME from IME target window. - // TODO(b/139861270): send to mCurClient.client once IMMS is aware of - // actual IME target. mWindowManagerInternal.hideIme( mHideRequestWindowMap.get(windowToken), mCurClient.mSelfReportedDisplayId); diff --git a/services/core/java/com/android/server/locales/LocaleManagerService.java b/services/core/java/com/android/server/locales/LocaleManagerService.java index 364f6db28f03..4ce03205fc1c 100644 --- a/services/core/java/com/android/server/locales/LocaleManagerService.java +++ b/services/core/java/com/android/server/locales/LocaleManagerService.java @@ -364,16 +364,19 @@ public class LocaleManagerService extends SystemService { // 1.) A normal, non-privileged app querying its own locale. // 2.) The installer of the given app querying locales of a package installed by said // installer. - // 3.) The current input method querying locales of another package. + // 3.) The current input method querying locales of the current foreground app. // 4.) A privileged system service querying locales of another package. // The least privileged case is a normal app performing a query, so check that first and get // locales if the package name is owned by the app. Next check if the calling app is the // installer of the given app and get locales. Finally check if the calling app is the - // current input method. If neither conditions matched, check if the caller has the - // necessary permission and fetch locales. + // current input method, and that app is querying locales of the current foreground app. If + // neither conditions matched, check if the caller has the necessary permission and fetch + // locales. if (!isPackageOwnedByCaller(appPackageName, userId) && !isCallerInstaller(appPackageName, userId) - && !isCallerFromCurrentInputMethod(userId)) { + && !(isCallerFromCurrentInputMethod(userId) + && mActivityManagerInternal.isAppForeground( + getPackageUid(appPackageName, userId)))) { enforceReadAppSpecificLocalesPermission(); } final long token = Binder.clearCallingIdentity(); diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java index f653b9381721..439e9bd2624e 100644 --- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java +++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java @@ -132,7 +132,7 @@ class MediaRouter2ServiceImpl { } } - mEventLogger.log(new EventLogger.StringEvent("mScreenOnOffReceiver", null)); + mEventLogger.enqueue(new EventLogger.StringEvent("mScreenOnOffReceiver", null)); } }; @@ -634,7 +634,7 @@ class MediaRouter2ServiceImpl { /* package */ void updateRunningUserAndProfiles(int newActiveUserId) { synchronized (mLock) { if (mCurrentActiveUserId != newActiveUserId) { - mEventLogger.log( + mEventLogger.enqueue( EventLogger.StringEvent.from("switchUser", "userId: %d", newActiveUserId)); @@ -705,7 +705,7 @@ class MediaRouter2ServiceImpl { obtainMessage(UserHandler::notifyRouterRegistered, userRecord.mHandler, routerRecord)); - mEventLogger.log(EventLogger.StringEvent.from("registerRouter2", + mEventLogger.enqueue(EventLogger.StringEvent.from("registerRouter2", "package: %s, uid: %d, pid: %d, router id: %d", packageName, uid, pid, routerRecord.mRouterId)); } @@ -718,7 +718,7 @@ class MediaRouter2ServiceImpl { return; } - mEventLogger.log( + mEventLogger.enqueue( EventLogger.StringEvent.from( "unregisterRouter2", "package: %s, router id: %d", @@ -744,7 +744,7 @@ class MediaRouter2ServiceImpl { return; } - mEventLogger.log(EventLogger.StringEvent.from( + mEventLogger.enqueue(EventLogger.StringEvent.from( "setDiscoveryRequestWithRouter2", "router id: %d, discovery request: %s", routerRecord.mRouterId, discoveryRequest.toString())); @@ -766,7 +766,7 @@ class MediaRouter2ServiceImpl { RouterRecord routerRecord = mAllRouterRecords.get(binder); if (routerRecord != null) { - mEventLogger.log(EventLogger.StringEvent.from( + mEventLogger.enqueue(EventLogger.StringEvent.from( "setRouteVolumeWithRouter2", "router id: %d, volume: %d", routerRecord.mRouterId, volume)); @@ -851,7 +851,7 @@ class MediaRouter2ServiceImpl { return; } - mEventLogger.log(EventLogger.StringEvent.from( + mEventLogger.enqueue(EventLogger.StringEvent.from( "selectRouteWithRouter2", "router id: %d, route: %s", routerRecord.mRouterId, route.getId())); @@ -871,7 +871,7 @@ class MediaRouter2ServiceImpl { return; } - mEventLogger.log(EventLogger.StringEvent.from( + mEventLogger.enqueue(EventLogger.StringEvent.from( "deselectRouteWithRouter2", "router id: %d, route: %s", routerRecord.mRouterId, route.getId())); @@ -891,7 +891,7 @@ class MediaRouter2ServiceImpl { return; } - mEventLogger.log(EventLogger.StringEvent.from( + mEventLogger.enqueue(EventLogger.StringEvent.from( "transferToRouteWithRouter2", "router id: %d, route: %s", routerRecord.mRouterId, route.getId())); @@ -921,7 +921,7 @@ class MediaRouter2ServiceImpl { return; } - mEventLogger.log(EventLogger.StringEvent.from( + mEventLogger.enqueue(EventLogger.StringEvent.from( "setSessionVolumeWithRouter2", "router id: %d, session: %s, volume: %d", routerRecord.mRouterId, uniqueSessionId, volume)); @@ -941,7 +941,7 @@ class MediaRouter2ServiceImpl { return; } - mEventLogger.log(EventLogger.StringEvent.from( + mEventLogger.enqueue(EventLogger.StringEvent.from( "releaseSessionWithRouter2", "router id: %d, session: %s", routerRecord.mRouterId, uniqueSessionId)); @@ -983,7 +983,7 @@ class MediaRouter2ServiceImpl { return; } - mEventLogger.log( + mEventLogger.enqueue( EventLogger.StringEvent.from("registerManager", "uid: %d, pid: %d, package: %s, userId: %d", uid, pid, packageName, userId)); @@ -1025,7 +1025,7 @@ class MediaRouter2ServiceImpl { } UserRecord userRecord = managerRecord.mUserRecord; - mEventLogger.log( + mEventLogger.enqueue( EventLogger.StringEvent.from( "unregisterManager", "package: %s, userId: %d, managerId: %d", @@ -1045,7 +1045,7 @@ class MediaRouter2ServiceImpl { return; } - mEventLogger.log( + mEventLogger.enqueue( EventLogger.StringEvent.from("startScan", "manager: %d", managerRecord.mManagerId)); @@ -1059,7 +1059,7 @@ class MediaRouter2ServiceImpl { return; } - mEventLogger.log( + mEventLogger.enqueue( EventLogger.StringEvent.from("stopScan", "manager: %d", managerRecord.mManagerId)); @@ -1076,7 +1076,7 @@ class MediaRouter2ServiceImpl { return; } - mEventLogger.log( + mEventLogger.enqueue( EventLogger.StringEvent.from("setRouteVolumeWithManager", "managerId: %d, routeId: %s, volume: %d", managerRecord.mManagerId, route.getId(), volume)); @@ -1096,7 +1096,7 @@ class MediaRouter2ServiceImpl { return; } - mEventLogger.log( + mEventLogger.enqueue( EventLogger.StringEvent.from("requestCreateSessionWithManager", "managerId: %d, routeId: %s", managerRecord.mManagerId, route.getId())); @@ -1146,7 +1146,7 @@ class MediaRouter2ServiceImpl { return; } - mEventLogger.log( + mEventLogger.enqueue( EventLogger.StringEvent.from("selectRouteWithManager", "managerId: %d, session: %s, routeId: %s", managerRecord.mManagerId, uniqueSessionId, route.getId())); @@ -1172,7 +1172,7 @@ class MediaRouter2ServiceImpl { return; } - mEventLogger.log( + mEventLogger.enqueue( EventLogger.StringEvent.from("deselectRouteWithManager", "managerId: %d, session: %s, routeId: %s", managerRecord.mManagerId, uniqueSessionId, route.getId())); @@ -1198,7 +1198,7 @@ class MediaRouter2ServiceImpl { return; } - mEventLogger.log( + mEventLogger.enqueue( EventLogger.StringEvent.from("transferToRouteWithManager", "managerId: %d, session: %s, routeId: %s", managerRecord.mManagerId, uniqueSessionId, route.getId())); @@ -1224,7 +1224,7 @@ class MediaRouter2ServiceImpl { return; } - mEventLogger.log( + mEventLogger.enqueue( EventLogger.StringEvent.from("setSessionVolumeWithManager", "managerId: %d, session: %s, volume: %d", managerRecord.mManagerId, uniqueSessionId, volume)); @@ -1245,7 +1245,7 @@ class MediaRouter2ServiceImpl { return; } - mEventLogger.log( + mEventLogger.enqueue( EventLogger.StringEvent.from("releaseSessionWithManager", "managerId: %d, session: %s", managerRecord.mManagerId, uniqueSessionId)); diff --git a/services/core/java/com/android/server/pm/AppsFilterImpl.java b/services/core/java/com/android/server/pm/AppsFilterImpl.java index 4c21195e2890..e3a2fb2a2dee 100644 --- a/services/core/java/com/android/server/pm/AppsFilterImpl.java +++ b/services/core/java/com/android/server/pm/AppsFilterImpl.java @@ -22,6 +22,15 @@ import static android.os.UserHandle.USER_NULL; import static android.provider.DeviceConfig.NAMESPACE_PACKAGE_MANAGER_SERVICE; import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE; +import static com.android.internal.util.FrameworkStatsLog.PACKAGE_MANAGER_APPS_FILTER_CACHE_BUILD_REPORTED; +import static com.android.internal.util.FrameworkStatsLog.PACKAGE_MANAGER_APPS_FILTER_CACHE_BUILD_REPORTED__EVENT_TYPE__BOOT; +import static com.android.internal.util.FrameworkStatsLog.PACKAGE_MANAGER_APPS_FILTER_CACHE_BUILD_REPORTED__EVENT_TYPE__USER_CREATED; +import static com.android.internal.util.FrameworkStatsLog.PACKAGE_MANAGER_APPS_FILTER_CACHE_BUILD_REPORTED__EVENT_TYPE__USER_DELETED; +import static com.android.internal.util.FrameworkStatsLog.PACKAGE_MANAGER_APPS_FILTER_CACHE_UPDATE_REPORTED; +import static com.android.internal.util.FrameworkStatsLog.PACKAGE_MANAGER_APPS_FILTER_CACHE_UPDATE_REPORTED__EVENT_TYPE__COMPAT_CHANGED; +import static com.android.internal.util.FrameworkStatsLog.PACKAGE_MANAGER_APPS_FILTER_CACHE_UPDATE_REPORTED__EVENT_TYPE__PACKAGE_ADDED; +import static com.android.internal.util.FrameworkStatsLog.PACKAGE_MANAGER_APPS_FILTER_CACHE_UPDATE_REPORTED__EVENT_TYPE__PACKAGE_DELETED; +import static com.android.internal.util.FrameworkStatsLog.PACKAGE_MANAGER_APPS_FILTER_CACHE_UPDATE_REPORTED__EVENT_TYPE__PACKAGE_REPLACED; import static com.android.server.pm.AppsFilterUtils.canQueryAsInstaller; import static com.android.server.pm.AppsFilterUtils.canQueryViaComponents; import static com.android.server.pm.AppsFilterUtils.canQueryViaPackage; @@ -36,6 +45,7 @@ import android.content.pm.PackageManagerInternal; import android.content.pm.SigningDetails; import android.content.pm.UserInfo; import android.os.Handler; +import android.os.SystemClock; import android.os.SystemProperties; import android.os.Trace; import android.os.UserHandle; @@ -49,6 +59,7 @@ import com.android.internal.R; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; +import com.android.internal.util.FrameworkStatsLog; import com.android.server.FgThread; import com.android.server.compat.CompatChange; import com.android.server.om.OverlayReferenceMapper; @@ -351,8 +362,15 @@ public final class AppsFilterImpl extends AppsFilterLocked implements Watchable, if (pkg == null) { return; } + final long currentTimeUs = SystemClock.currentTimeMicro(); updateEnabledState(pkg); mAppsFilter.updateShouldFilterCacheForPackage(snapshot, packageName); + mAppsFilter.logCacheUpdated( + PACKAGE_MANAGER_APPS_FILTER_CACHE_UPDATE_REPORTED__EVENT_TYPE__COMPAT_CHANGED, + SystemClock.currentTimeMicro() - currentTimeUs, + snapshot.getUserInfos().length, + snapshot.getPackageStates().size(), + pkg.getUid()); } private void updateEnabledState(@NonNull AndroidPackage pkg) { @@ -465,7 +483,8 @@ public final class AppsFilterImpl extends AppsFilterLocked implements Watchable, mOverlayReferenceMapper.rebuildIfDeferred(); mFeatureConfig.onSystemReady(); - updateEntireShouldFilterCacheAsync(pmInternal); + updateEntireShouldFilterCacheAsync(pmInternal, + PACKAGE_MANAGER_APPS_FILTER_CACHE_BUILD_REPORTED__EVENT_TYPE__BOOT); } /** @@ -476,13 +495,17 @@ public final class AppsFilterImpl extends AppsFilterLocked implements Watchable, */ public void addPackage(Computer snapshot, PackageStateInternal newPkgSetting, boolean isReplace) { + final long currentTimeUs = SystemClock.currentTimeMicro(); + final int logType = isReplace + ? PACKAGE_MANAGER_APPS_FILTER_CACHE_UPDATE_REPORTED__EVENT_TYPE__PACKAGE_REPLACED + : PACKAGE_MANAGER_APPS_FILTER_CACHE_UPDATE_REPORTED__EVENT_TYPE__PACKAGE_ADDED; if (DEBUG_TRACING) { Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "filter.addPackage"); } try { if (isReplace) { // let's first remove any prior rules for this package - removePackage(snapshot, newPkgSetting, true /*isReplace*/); + removePackageInternal(snapshot, newPkgSetting, true /*isReplace*/); } final ArrayMap<String, ? extends PackageStateInternal> settings = snapshot.getPackageStates(); @@ -508,6 +531,8 @@ public final class AppsFilterImpl extends AppsFilterLocked implements Watchable, } } } + logCacheUpdated(logType, SystemClock.currentTimeMicro() - currentTimeUs, + users.length, settings.size(), newPkgSetting.getAppId()); } else { invalidateCache("addPackage: " + newPkgSetting.getPackageName()); } @@ -757,18 +782,19 @@ public final class AppsFilterImpl extends AppsFilterLocked implements Watchable, } } - private void updateEntireShouldFilterCacheAsync(PackageManagerInternal pmInternal) { - updateEntireShouldFilterCacheAsync(pmInternal, CACHE_REBUILD_DELAY_MIN_MS); + private void updateEntireShouldFilterCacheAsync(PackageManagerInternal pmInternal, int reason) { + updateEntireShouldFilterCacheAsync(pmInternal, CACHE_REBUILD_DELAY_MIN_MS, reason); } private void updateEntireShouldFilterCacheAsync(PackageManagerInternal pmInternal, - long delayMs) { + long delayMs, int reason) { mBackgroundHandler.postDelayed(() -> { if (!mCacheValid.compareAndSet(CACHE_INVALID, CACHE_VALID)) { // Cache is already valid. return; } + final long currentTimeUs = SystemClock.currentTimeMicro(); final ArrayMap<String, AndroidPackage> packagesCache = new ArrayMap<>(); final UserInfo[][] usersRef = new UserInfo[1][]; final Computer snapshot = (Computer) pmInternal.snapshot(); @@ -787,11 +813,13 @@ public final class AppsFilterImpl extends AppsFilterLocked implements Watchable, updateEntireShouldFilterCacheInner(snapshot, settings, usersRef[0], USER_ALL); onChanged(); + logCacheRebuilt(reason, SystemClock.currentTimeMicro() - currentTimeUs, + users.length, settings.size()); if (!mCacheValid.compareAndSet(CACHE_VALID, CACHE_VALID)) { Slog.i(TAG, "Cache invalidated while building, retrying."); updateEntireShouldFilterCacheAsync(pmInternal, - Math.min(delayMs * 2, CACHE_REBUILD_DELAY_MAX_MS)); + Math.min(delayMs * 2, CACHE_REBUILD_DELAY_MAX_MS), reason); return; } @@ -803,15 +831,27 @@ public final class AppsFilterImpl extends AppsFilterLocked implements Watchable, if (!mCacheReady) { return; } + final long currentTimeUs = SystemClock.currentTimeMicro(); updateEntireShouldFilterCache(snapshot, newUserId); + logCacheRebuilt( + PACKAGE_MANAGER_APPS_FILTER_CACHE_BUILD_REPORTED__EVENT_TYPE__USER_CREATED, + SystemClock.currentTimeMicro() - currentTimeUs, + snapshot.getUserInfos().length, + snapshot.getPackageStates().size()); } - public void onUserDeleted(@UserIdInt int userId) { + public void onUserDeleted(Computer snapshot, @UserIdInt int userId) { if (!mCacheReady) { return; } + final long currentTimeUs = SystemClock.currentTimeMicro(); removeShouldFilterCacheForUser(userId); onChanged(); + logCacheRebuilt( + PACKAGE_MANAGER_APPS_FILTER_CACHE_BUILD_REPORTED__EVENT_TYPE__USER_DELETED, + SystemClock.currentTimeMicro() - currentTimeUs, + snapshot.getUserInfos().length, + snapshot.getPackageStates().size()); } private void updateShouldFilterCacheForPackage(Computer snapshot, @@ -988,10 +1028,26 @@ public final class AppsFilterImpl extends AppsFilterLocked implements Watchable, /** * Removes a package for consideration when filtering visibility between apps. * + * @param setting the setting of the package being removed. + */ + public void removePackage(Computer snapshot, PackageStateInternal setting) { + final long currentTimeUs = SystemClock.currentTimeMicro(); + removePackageInternal(snapshot, setting, false /* isReplace */); + logCacheUpdated( + PACKAGE_MANAGER_APPS_FILTER_CACHE_UPDATE_REPORTED__EVENT_TYPE__PACKAGE_DELETED, + SystemClock.currentTimeMicro() - currentTimeUs, + snapshot.getUserInfos().length, + snapshot.getPackageStates().size(), + setting.getAppId()); + } + + /** + * Removes a package for consideration when filtering visibility between apps. + * * @param setting the setting of the package being removed. * @param isReplace if the package is being replaced. */ - public void removePackage(Computer snapshot, PackageStateInternal setting, + private void removePackageInternal(Computer snapshot, PackageStateInternal setting, boolean isReplace) { final ArraySet<String> additionalChangedPackages; final ArrayMap<String, ? extends PackageStateInternal> settings = @@ -1174,4 +1230,18 @@ public final class AppsFilterImpl extends AppsFilterLocked implements Watchable, } } } + + private void logCacheRebuilt(int eventId, long latency, int userCount, int packageCount) { + FrameworkStatsLog.write(PACKAGE_MANAGER_APPS_FILTER_CACHE_BUILD_REPORTED, + eventId, latency, userCount, packageCount, mShouldFilterCache.size()); + } + + private void logCacheUpdated(int eventId, long latency, int userCount, int packageCount, + int appId) { + if (!mCacheReady) { + return; + } + FrameworkStatsLog.write(PACKAGE_MANAGER_APPS_FILTER_CACHE_UPDATE_REPORTED, + eventId, appId, latency, userCount, packageCount, mShouldFilterCache.size()); + } } diff --git a/services/core/java/com/android/server/pm/BackgroundDexOptService.java b/services/core/java/com/android/server/pm/BackgroundDexOptService.java index e7412c5e52ec..d856d54cb4e0 100644 --- a/services/core/java/com/android/server/pm/BackgroundDexOptService.java +++ b/services/core/java/com/android/server/pm/BackgroundDexOptService.java @@ -410,7 +410,7 @@ public final class BackgroundDexOptService { job.jobFinished(params, !completed); } else { // Periodic job - job.jobFinished(params, true); + job.jobFinished(params, false /* reschedule */); } markDexOptCompleted(); } diff --git a/services/core/java/com/android/server/pm/BackgroundInstallControlService.java b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java new file mode 100644 index 000000000000..df95f86d1e07 --- /dev/null +++ b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.pm; + +import android.annotation.NonNull; +import android.content.Context; +import android.content.pm.IBackgroundInstallControlService; +import android.content.pm.IPackageManager; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.ParceledListSlice; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.util.SparseArrayMap; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.SystemService; + +/** + * @hide + */ +public class BackgroundInstallControlService extends SystemService { + private static final String TAG = "BackgroundInstallControlService"; + + private final Context mContext; + private final BinderService mBinderService; + private final IPackageManager mIPackageManager; + + // User ID -> package name -> time diff + // The time diff between the last foreground activity installer and + // the "onPackageAdded" function call. + private final SparseArrayMap<String, Long> mBackgroundInstalledPackages = + new SparseArrayMap<>(); + + public BackgroundInstallControlService(@NonNull Context context) { + this(new InjectorImpl(context)); + } + + @VisibleForTesting + BackgroundInstallControlService(@NonNull Injector injector) { + super(injector.getContext()); + mContext = injector.getContext(); + mIPackageManager = injector.getIPackageManager(); + mBinderService = new BinderService(this); + } + + private static final class BinderService extends IBackgroundInstallControlService.Stub { + final BackgroundInstallControlService mService; + + BinderService(BackgroundInstallControlService service) { + mService = service; + } + + @Override + public ParceledListSlice<PackageInfo> getBackgroundInstalledPackages( + @PackageManager.PackageInfoFlagsBits long flags, int userId) { + ParceledListSlice<PackageInfo> packages; + try { + packages = mService.mIPackageManager.getInstalledPackages(flags, userId); + } catch (RemoteException e) { + throw new IllegalStateException("Package manager not available", e); + } + + // TODO(b/244216300): to enable the test the usage by BinaryTransparencyService, + // we currently comment out the actual implementation. + // The fake implementation is just to filter out the first app of the list. + // for (int i = 0, size = packages.getList().size(); i < size; i++) { + // String packageName = packages.getList().get(i).packageName; + // if (!mBackgroundInstalledPackages.contains(userId, packageName) { + // packages.getList().remove(i); + // } + // } + if (packages.getList().size() > 0) { + packages.getList().remove(0); + } + return packages; + } + } + + /** + * Called when the system service should publish a binder service using + * {@link #publishBinderService(String, IBinder).} + */ + @Override + public void onStart() { + publishBinderService(Context.BACKGROUND_INSTALL_CONTROL_SERVICE, mBinderService); + } + + /** + * Dependency injector for {@link #BackgroundInstallControlService)}. + */ + interface Injector { + Context getContext(); + + IPackageManager getIPackageManager(); + } + + private static final class InjectorImpl implements Injector { + private final Context mContext; + + InjectorImpl(Context context) { + mContext = context; + } + + @Override + public Context getContext() { + return mContext; + } + + @Override + public IPackageManager getIPackageManager() { + return IPackageManager.Stub.asInterface(ServiceManager.getService("package")); + } + } +} diff --git a/services/core/java/com/android/server/pm/OWNERS b/services/core/java/com/android/server/pm/OWNERS index 8534fabb5576..84324f2524fc 100644 --- a/services/core/java/com/android/server/pm/OWNERS +++ b/services/core/java/com/android/server/pm/OWNERS @@ -3,8 +3,6 @@ hackbod@google.com jsharkey@android.com jsharkey@google.com narayan@google.com -svetoslavganov@android.com -svetoslavganov@google.com include /PACKAGE_MANAGER_OWNERS # apex support @@ -26,16 +24,10 @@ per-file PackageManagerServiceCompilerMapping.java = file:dex/OWNERS per-file PackageUsage.java = file:dex/OWNERS # multi user / cross profile -per-file CrossProfileAppsServiceImpl.java = omakoto@google.com, yamasani@google.com -per-file CrossProfileAppsService.java = omakoto@google.com, yamasani@google.com -per-file CrossProfileIntentFilter.java = omakoto@google.com, yamasani@google.com -per-file CrossProfileIntentResolver.java = omakoto@google.com, yamasani@google.com +per-file CrossProfile* = file:MULTIUSER_AND_ENTERPRISE_OWNERS per-file RestrictionsSet.java = file:MULTIUSER_AND_ENTERPRISE_OWNERS -per-file UserManager* = file:/MULTIUSER_OWNERS per-file UserRestriction* = file:MULTIUSER_AND_ENTERPRISE_OWNERS -per-file UserSystemPackageInstaller* = file:/MULTIUSER_OWNERS -per-file UserTypeDetails.java = file:/MULTIUSER_OWNERS -per-file UserTypeFactory.java = file:/MULTIUSER_OWNERS +per-file User* = file:/MULTIUSER_OWNERS # security per-file KeySetHandle.java = cbrubaker@google.com, nnk@google.com diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index dfbe68a8e997..23cf26236e87 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -1147,7 +1147,7 @@ public class PackageManagerService implements PackageSender, TestUtilityService var done = SystemClock.currentTimeMicro(); if (mSnapshotStatistics != null) { - mSnapshotStatistics.rebuild(now, done, hits); + mSnapshotStatistics.rebuild(now, done, hits, newSnapshot.getPackageStates().size()); } return newSnapshot; } @@ -4220,7 +4220,7 @@ public class PackageManagerService implements PackageSender, TestUtilityService mSettings.removeUserLPw(userId); mPendingBroadcasts.remove(userId); mDeletePackageHelper.removeUnusedPackagesLPw(userManager, userId); - mAppsFilter.onUserDeleted(userId); + mAppsFilter.onUserDeleted(snapshotComputer(), userId); } mInstantAppRegistry.onUserRemoved(userId); } diff --git a/services/core/java/com/android/server/pm/RemovePackageHelper.java b/services/core/java/com/android/server/pm/RemovePackageHelper.java index bbc4fdeb36bb..7e936735ef7d 100644 --- a/services/core/java/com/android/server/pm/RemovePackageHelper.java +++ b/services/core/java/com/android/server/pm/RemovePackageHelper.java @@ -308,7 +308,7 @@ final class RemovePackageHelper { mPm.mSettings.getKeySetManagerService().removeAppKeySetDataLPw(packageName); final Computer snapshot = mPm.snapshotComputer(); mPm.mAppsFilter.removePackage(snapshot, - snapshot.getPackageStateInternal(packageName), false /* isReplace */); + snapshot.getPackageStateInternal(packageName)); removedAppId = mPm.mSettings.removePackageLPw(packageName); if (outInfo != null) { outInfo.mRemovedAppId = removedAppId; diff --git a/services/core/java/com/android/server/pm/SnapshotStatistics.java b/services/core/java/com/android/server/pm/SnapshotStatistics.java index 2cfc8946ad02..e04a1e5b3569 100644 --- a/services/core/java/com/android/server/pm/SnapshotStatistics.java +++ b/services/core/java/com/android/server/pm/SnapshotStatistics.java @@ -24,11 +24,13 @@ import android.os.SystemClock; import android.text.TextUtils; import com.android.internal.annotations.GuardedBy; +import com.android.internal.util.FrameworkStatsLog; import com.android.server.EventLogTags; import java.io.PrintWriter; import java.util.Arrays; import java.util.Locale; +import java.util.concurrent.TimeUnit; /** * This class records statistics about PackageManagerService snapshots. It maintains two sets of @@ -59,9 +61,9 @@ public class SnapshotStatistics { public static final int SNAPSHOT_TICK_INTERVAL_MS = 60 * 1000; /** - * The number of ticks for long statistics. This is one week. + * The interval of the snapshot statistics logging. */ - public static final int SNAPSHOT_LONG_TICKS = 7 * 24 * 60; + private static final long SNAPSHOT_LOG_INTERVAL_US = TimeUnit.DAYS.toMicros(1); /** * The number snapshot event logs that can be generated in a single logging interval. @@ -93,6 +95,28 @@ public class SnapshotStatistics { public static final int SNAPSHOT_SHORT_LIFETIME = 5; /** + * Buckets to represent a range of the rebuild latency for the histogram of + * snapshot rebuild latency. + */ + private static final int REBUILD_LATENCY_BUCKET_LESS_THAN_1_MILLIS = 1; + private static final int REBUILD_LATENCY_BUCKET_LESS_THAN_2_MILLIS = 2; + private static final int REBUILD_LATENCY_BUCKET_LESS_THAN_5_MILLIS = 5; + private static final int REBUILD_LATENCY_BUCKET_LESS_THAN_10_MILLIS = 10; + private static final int REBUILD_LATENCY_BUCKET_LESS_THAN_20_MILLIS = 20; + private static final int REBUILD_LATENCY_BUCKET_LESS_THAN_50_MILLIS = 50; + private static final int REBUILD_LATENCY_BUCKET_LESS_THAN_100_MILLIS = 100; + + /** + * Buckets to represent a range of the reuse count for the histogram of + * snapshot reuse counts. + */ + private static final int REUSE_COUNT_BUCKET_LESS_THAN_1 = 1; + private static final int REUSE_COUNT_BUCKET_LESS_THAN_10 = 10; + private static final int REUSE_COUNT_BUCKET_LESS_THAN_100 = 100; + private static final int REUSE_COUNT_BUCKET_LESS_THAN_1000 = 1000; + private static final int REUSE_COUNT_BUCKET_LESS_THAN_10000 = 10000; + + /** * The lock to control access to this object. */ private final Object mLock = new Object(); @@ -113,11 +137,6 @@ public class SnapshotStatistics { private int mEventsReported = 0; /** - * The tick counter. At the default tick interval, this wraps every 4000 years or so. - */ - private int mTicks = 0; - - /** * The handler used for the periodic ticks. */ private Handler mHandler = null; @@ -139,8 +158,6 @@ public class SnapshotStatistics { // The number of bins private int mCount; - // The mapping of low integers to bins - private int[] mBinMap; // The maximum mapped value. Values at or above this are mapped to the // top bin. private int mMaxBin; @@ -158,16 +175,6 @@ public class SnapshotStatistics { mCount = mUserKey.length + 1; // The maximum value is one more than the last one in the map. mMaxBin = mUserKey[mUserKey.length - 1] + 1; - mBinMap = new int[mMaxBin + 1]; - - int j = 0; - for (int i = 0; i < mUserKey.length; i++) { - while (j <= mUserKey[i]) { - mBinMap[j] = i; - j++; - } - } - mBinMap[mMaxBin] = mUserKey.length; } /** @@ -175,9 +182,14 @@ public class SnapshotStatistics { */ public int getBin(int x) { if (x >= 0 && x < mMaxBin) { - return mBinMap[x]; + for (int i = 0; i < mUserKey.length; i++) { + if (x <= mUserKey[i]) { + return i; + } + } + return 0; // should not happen } else if (x >= mMaxBin) { - return mBinMap[mMaxBin]; + return mUserKey.length; } else { // x is negative. The bin will not be used. return 0; @@ -263,6 +275,11 @@ public class SnapshotStatistics { public int mMaxBuildTimeUs = 0; /** + * The maximum used count since the last log. + */ + public int mMaxUsedCount = 0; + + /** * Record the rebuild. The parameters are the length of time it took to build the * latest snapshot, and the number of times the _previous_ snapshot was used. A * negative value for used signals an invalid value, which is the case the first @@ -279,7 +296,6 @@ public class SnapshotStatistics { } mTotalTimeUs += duration; - boolean reportIt = false; if (big) { mBigBuilds++; @@ -290,6 +306,9 @@ public class SnapshotStatistics { if (mMaxBuildTimeUs < duration) { mMaxBuildTimeUs = duration; } + if (mMaxUsedCount < used) { + mMaxUsedCount = used; + } } private Stats(long now) { @@ -313,6 +332,7 @@ public class SnapshotStatistics { mShortLived = orig.mShortLived; mTotalTimeUs = orig.mTotalTimeUs; mMaxBuildTimeUs = orig.mMaxBuildTimeUs; + mMaxUsedCount = orig.mMaxUsedCount; } /** @@ -443,18 +463,19 @@ public class SnapshotStatistics { } /** - * Report the object via an event. Presumably the record indicates an anomalous - * incident. + * Report the snapshot statistics to FrameworkStatsLog. */ - private void report() { - EventLogTags.writePmSnapshotStats( - mTotalBuilds, mTotalUsed, mBigBuilds, mShortLived, - mMaxBuildTimeUs / US_IN_MS, mTotalTimeUs / US_IN_MS); + private void logSnapshotStatistics(int packageCount) { + final long avgLatencyUs = (mTotalBuilds == 0 ? 0 : mTotalTimeUs / mTotalBuilds); + final int avgUsedCount = (mTotalBuilds == 0 ? 0 : mTotalUsed / mTotalBuilds); + FrameworkStatsLog.write( + FrameworkStatsLog.PACKAGE_MANAGER_SNAPSHOT_REPORTED, mTimes, mUsed, + mMaxBuildTimeUs, mMaxUsedCount, avgLatencyUs, avgUsedCount, packageCount); } } /** - * Long statistics. These roll over approximately every week. + * Long statistics. These roll over approximately one day. */ private Stats[] mLong; @@ -464,10 +485,14 @@ public class SnapshotStatistics { private Stats[] mShort; /** - * The time of the last build. This can be used to compute the length of time a - * snapshot existed before being replaced. + * The time of last logging to the FrameworkStatsLog. */ - private long mLastBuildTime = 0; + private long mLastLogTimeUs; + + /** + * The number of packages on the device. + */ + private int mPackageCount; /** * Create a snapshot object. Initialize the bin levels. The last bin catches @@ -475,8 +500,20 @@ public class SnapshotStatistics { */ public SnapshotStatistics() { // Create the bin thresholds. The time bins are in units of us. - mTimeBins = new BinMap(new int[] { 1, 2, 5, 10, 20, 50, 100 }); - mUseBins = new BinMap(new int[] { 1, 2, 5, 10, 20, 50, 100 }); + mTimeBins = new BinMap(new int[] { + REBUILD_LATENCY_BUCKET_LESS_THAN_1_MILLIS, + REBUILD_LATENCY_BUCKET_LESS_THAN_2_MILLIS, + REBUILD_LATENCY_BUCKET_LESS_THAN_5_MILLIS, + REBUILD_LATENCY_BUCKET_LESS_THAN_10_MILLIS, + REBUILD_LATENCY_BUCKET_LESS_THAN_20_MILLIS, + REBUILD_LATENCY_BUCKET_LESS_THAN_50_MILLIS, + REBUILD_LATENCY_BUCKET_LESS_THAN_100_MILLIS }); + mUseBins = new BinMap(new int[] { + REUSE_COUNT_BUCKET_LESS_THAN_1, + REUSE_COUNT_BUCKET_LESS_THAN_10, + REUSE_COUNT_BUCKET_LESS_THAN_100, + REUSE_COUNT_BUCKET_LESS_THAN_1000, + REUSE_COUNT_BUCKET_LESS_THAN_10000 }); // Create the raw statistics final long now = SystemClock.currentTimeMicro(); @@ -484,6 +521,7 @@ public class SnapshotStatistics { mLong[0] = new Stats(now); mShort = new Stats[10]; mShort[0] = new Stats(now); + mLastLogTimeUs = now; // Create the message handler for ticks and start the ticker. mHandler = new Handler(Looper.getMainLooper()) { @@ -516,13 +554,14 @@ public class SnapshotStatistics { * @param now The time at which the snapshot rebuild began, in ns. * @param done The time at which the snapshot rebuild completed, in ns. * @param hits The number of times the previous snapshot was used. + * @param packageCount The number of packages on the device. */ - public final void rebuild(long now, long done, int hits) { + public final void rebuild(long now, long done, int hits, int packageCount) { // The duration has a span of about 2000s final int duration = (int) (done - now); boolean reportEvent = false; synchronized (mLock) { - mLastBuildTime = now; + mPackageCount = packageCount; final int timeBin = mTimeBins.getBin(duration / 1000); final int useBin = mUseBins.getBin(hits); @@ -570,10 +609,12 @@ public class SnapshotStatistics { private void tick() { synchronized (mLock) { long now = SystemClock.currentTimeMicro(); - mTicks++; - if (mTicks % SNAPSHOT_LONG_TICKS == 0) { + if (now - mLastLogTimeUs > SNAPSHOT_LOG_INTERVAL_US) { shift(mLong, now); + mLastLogTimeUs = now; + mLong[mLong.length - 1].logSnapshotStatistics(mPackageCount); } + shift(mShort, now); mEventsReported = 0; } diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index 2bfee8cfc288..cf0ea437ea38 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -104,7 +104,6 @@ import android.util.StatsEvent; import android.util.TimeUtils; import android.util.TypedValue; import android.util.Xml; -import android.view.Display; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; @@ -625,14 +624,7 @@ public class UserManagerService extends IUserManager.Stub { @GuardedBy("mUserStates") private final WatchedUserStates mUserStates = new WatchedUserStates(); - /** - * Set on on devices that support background users (key) running on secondary displays (value). - */ - // TODO(b/244644281): move such logic to a different class (like UserDisplayAssigner) - @Nullable - @GuardedBy("mUsersOnSecondaryDisplays") - private final SparseIntArray mUsersOnSecondaryDisplays; - private final boolean mUsersOnSecondaryDisplaysEnabled; + private final UserVisibilityMediator mUserVisibilityMediator; private static UserManagerService sInstance; @@ -708,8 +700,7 @@ public class UserManagerService extends IUserManager.Stub { @VisibleForTesting UserManagerService(Context context) { this(context, /* pm= */ null, /* userDataPreparer= */ null, - /* packagesLock= */ new Object(), context.getCacheDir(), /* users= */ null, - /* usersOnSecondaryDisplays= */ null); + /* packagesLock= */ new Object(), context.getCacheDir(), /* users= */ null); } /** @@ -720,13 +711,13 @@ public class UserManagerService extends IUserManager.Stub { UserManagerService(Context context, PackageManagerService pm, UserDataPreparer userDataPreparer, Object packagesLock) { this(context, pm, userDataPreparer, packagesLock, Environment.getDataDirectory(), - /* users= */ null, /* usersOnSecondaryDisplays= */ null); + /* users= */ null); } @VisibleForTesting UserManagerService(Context context, PackageManagerService pm, UserDataPreparer userDataPreparer, Object packagesLock, File dataDir, - SparseArray<UserData> users, @Nullable SparseIntArray usersOnSecondaryDisplays) { + SparseArray<UserData> users) { mContext = context; mPm = pm; mPackagesLock = packagesLock; @@ -756,14 +747,7 @@ public class UserManagerService extends IUserManager.Stub { mUserStates.put(UserHandle.USER_SYSTEM, UserState.STATE_BOOTING); mUser0Allocations = DBG_ALLOCATION ? new AtomicInteger() : null; emulateSystemUserModeIfNeeded(); - mUsersOnSecondaryDisplaysEnabled = UserManager.isUsersOnSecondaryDisplaysEnabled(); - if (mUsersOnSecondaryDisplaysEnabled) { - mUsersOnSecondaryDisplays = usersOnSecondaryDisplays == null - ? new SparseIntArray() // default behavior - : usersOnSecondaryDisplays; // passed by unit test - } else { - mUsersOnSecondaryDisplays = null; - } + mUserVisibilityMediator = new UserVisibilityMediator(this); } void systemReady() { @@ -1652,7 +1636,8 @@ public class UserManagerService extends IUserManager.Stub { return isProfileUnchecked(userId); } - private boolean isProfileUnchecked(@UserIdInt int userId) { + // TODO(b/244644281): make it private once UserVisibilityMediator don't use it anymore + boolean isProfileUnchecked(@UserIdInt int userId) { synchronized (mUsersLock) { UserInfo userInfo = getUserInfoLU(userId); return userInfo != null && userInfo.isProfile(); @@ -1729,11 +1714,6 @@ public class UserManagerService extends IUserManager.Stub { return userId == getCurrentUserId(); } - @VisibleForTesting - boolean isUsersOnSecondaryDisplaysEnabled() { - return mUsersOnSecondaryDisplaysEnabled; - } - @Override public boolean isUserVisible(@UserIdInt int userId) { int callingUserId = UserHandle.getCallingUserId(); @@ -1744,69 +1724,7 @@ public class UserManagerService extends IUserManager.Stub { + ") is visible"); } - return isUserVisibleUnchecked(userId); - } - - @VisibleForTesting - boolean isUserVisibleUnchecked(@UserIdInt int userId) { - // First check current foreground user and their profiles (on main display) - if (isCurrentUserOrRunningProfileOfCurrentUser(userId)) { - return true; - } - - // Device doesn't support multiple users on multiple displays, so only users checked above - // can be visible - if (!mUsersOnSecondaryDisplaysEnabled) { - return false; - } - - synchronized (mUsersOnSecondaryDisplays) { - return mUsersOnSecondaryDisplays.indexOfKey(userId) >= 0; - } - } - - @VisibleForTesting - int getDisplayAssignedToUser(@UserIdInt int userId) { - if (isCurrentUserOrRunningProfileOfCurrentUser(userId)) { - return Display.DEFAULT_DISPLAY; - } - - if (!mUsersOnSecondaryDisplaysEnabled) { - return Display.INVALID_DISPLAY; - } - - synchronized (mUsersOnSecondaryDisplays) { - return mUsersOnSecondaryDisplays.get(userId, Display.INVALID_DISPLAY); - } - } - - @VisibleForTesting - int getUserAssignedToDisplay(int displayId) { - if (displayId == Display.DEFAULT_DISPLAY || !mUsersOnSecondaryDisplaysEnabled) { - return getCurrentUserId(); - } - - synchronized (mUsersOnSecondaryDisplays) { - for (int i = 0; i < mUsersOnSecondaryDisplays.size(); i++) { - if (mUsersOnSecondaryDisplays.valueAt(i) != displayId) { - continue; - } - int userId = mUsersOnSecondaryDisplays.keyAt(i); - if (!isProfileUnchecked(userId)) { - return userId; - } else if (DBG_MUMD) { - Slogf.d(LOG_TAG, "getUserAssignedToDisplay(%d): skipping user %d because it's " - + "a profile", displayId, userId); - } - } - } - - int currentUserId = getCurrentUserId(); - if (DBG_MUMD) { - Slogf.d(LOG_TAG, "getUserAssignedToDisplay(%d): no user assigned to display, returning " - + "current user (%d) instead", displayId, currentUserId); - } - return currentUserId; + return mUserVisibilityMediator.isUserVisible(userId); } /** @@ -1850,54 +1768,9 @@ public class UserManagerService extends IUserManager.Stub { return false; } - // TODO(b/239982558): try to merge with isUserVisibleUnchecked() (once both are unit tested) - /** - * See {@link UserManagerInternal#isUserVisible(int, int)}. - */ + // Called by UserManagerServiceShellCommand boolean isUserVisibleOnDisplay(@UserIdInt int userId, int displayId) { - if (displayId == Display.INVALID_DISPLAY) { - return false; - } - if (!mUsersOnSecondaryDisplaysEnabled) { - return isCurrentUserOrRunningProfileOfCurrentUser(userId); - } - - // TODO(b/244644281): temporary workaround to let WM use this API without breaking current - // behavior - return true for current user / profile for any display (other than those - // explicitly assigned to another users), otherwise they wouldn't be able to launch - // activities on other non-passenger displays, like cluster, display, or virtual displays). - // In the long-term, it should rely just on mUsersOnSecondaryDisplays, which - // would be updated by DisplayManagerService when displays are created / initialized. - if (isCurrentUserOrRunningProfileOfCurrentUser(userId)) { - synchronized (mUsersOnSecondaryDisplays) { - boolean assignedToUser = false; - boolean assignedToAnotherUser = false; - for (int i = 0; i < mUsersOnSecondaryDisplays.size(); i++) { - if (mUsersOnSecondaryDisplays.valueAt(i) == displayId) { - if (mUsersOnSecondaryDisplays.keyAt(i) == userId) { - assignedToUser = true; - break; - } else { - assignedToAnotherUser = true; - // Cannot break because it could be assigned to a profile of the user - // (and we better not assume that the iteration will check for the - // parent user before its profiles) - } - } - } - if (DBG_MUMD) { - Slogf.d(LOG_TAG, "isUserVisibleOnDisplay(%d, %d): assignedToUser=%b, " - + "assignedToAnotherUser=%b, mUsersOnSecondaryDisplays=%s", - userId, displayId, assignedToUser, assignedToAnotherUser, - mUsersOnSecondaryDisplays); - } - return assignedToUser || !assignedToAnotherUser; - } - } - - synchronized (mUsersOnSecondaryDisplays) { - return mUsersOnSecondaryDisplays.get(userId, Display.INVALID_DISPLAY) == displayId; - } + return mUserVisibilityMediator.isUserVisible(userId, displayId); } @Override @@ -1915,7 +1788,7 @@ public class UserManagerService extends IUserManager.Stub { for (int i = 0; i < usersSize; i++) { UserInfo ui = mUsers.valueAt(i).info; if (!ui.partial && !ui.preCreated && !mRemovingUserIds.get(ui.id) - && isUserVisibleUnchecked(ui.id)) { + && mUserVisibilityMediator.isUserVisible(ui.id)) { visibleUsers.add(UserHandle.of(ui.id)); } } @@ -6251,9 +6124,15 @@ public class UserManagerService extends IUserManager.Stub { final long nowRealtime = SystemClock.elapsedRealtime(); final StringBuilder sb = new StringBuilder(); - if (args != null && args.length > 0 && args[0].equals("--user")) { - dumpUser(pw, UserHandle.parseUserArg(args[1]), sb, now, nowRealtime); - return; + if (args != null && args.length > 0) { + switch (args[0]) { + case "--user": + dumpUser(pw, UserHandle.parseUserArg(args[1]), sb, now, nowRealtime); + return; + case "--visibility-mediator": + mUserVisibilityMediator.dump(pw); + return; + } } final int currentUserId = getCurrentUserId(); @@ -6316,18 +6195,9 @@ public class UserManagerService extends IUserManager.Stub { } } // synchronized (mPackagesLock) - // Multiple Users on Multiple Display info - pw.println(" Supports users on secondary displays: " + mUsersOnSecondaryDisplaysEnabled); - // mUsersOnSecondaryDisplaysEnabled is set on constructor, while the UserManager API is - // set dynamically, so print both to help cases where the developer changed it on the fly - pw.println(" UM.isUsersOnSecondaryDisplaysEnabled(): " - + UserManager.isUsersOnSecondaryDisplaysEnabled()); - if (mUsersOnSecondaryDisplaysEnabled) { - pw.print(" Users on secondary displays: "); - synchronized (mUsersOnSecondaryDisplays) { - pw.println(mUsersOnSecondaryDisplays); - } - } + pw.println(); + mUserVisibilityMediator.dump(pw); + pw.println(); // Dump some capabilities pw.println(); @@ -6899,133 +6769,33 @@ public class UserManagerService extends IUserManager.Stub { } @Override - public void assignUserToDisplay(int userId, int displayId) { - if (DBG_MUMD) { - Slogf.d(LOG_TAG, "assignUserToDisplay(%d, %d)", userId, displayId); - } - - // NOTE: Using Boolean instead of boolean as it will be re-used below - Boolean isProfile = null; - if (displayId == Display.DEFAULT_DISPLAY) { - if (mUsersOnSecondaryDisplaysEnabled) { - // Profiles are only supported in the default display, but it cannot return yet - // as it needs to check if the parent is also assigned to the DEFAULT_DISPLAY - // (this is done indirectly below when it checks that the profile parent is the - // current user, as the current user is always assigned to the DEFAULT_DISPLAY). - isProfile = isProfileUnchecked(userId); - } - if (isProfile == null || !isProfile) { - // Don't need to do anything because methods (such as isUserVisible()) already - // know that the current user (and their profiles) is assigned to the default - // display. - if (DBG_MUMD) { - Slogf.d(LOG_TAG, "ignoring on default display"); - } - return; - } - } - - if (!mUsersOnSecondaryDisplaysEnabled) { - throw new UnsupportedOperationException("assignUserToDisplay(" + userId + ", " - + displayId + ") called on device that doesn't support multiple " - + "users on multiple displays"); - } - - Preconditions.checkArgument(userId != UserHandle.USER_SYSTEM, "Cannot assign system " - + "user to secondary display (%d)", displayId); - Preconditions.checkArgument(displayId != Display.INVALID_DISPLAY, - "Cannot assign to INVALID_DISPLAY (%d)", displayId); - - int currentUserId = getCurrentUserId(); - Preconditions.checkArgument(userId != currentUserId, - "Cannot assign current user (%d) to other displays", currentUserId); - - if (isProfile == null) { - isProfile = isProfileUnchecked(userId); - } - synchronized (mUsersOnSecondaryDisplays) { - if (isProfile) { - // Profile can only start in the same display as parent. And for simplicity, - // that display must be the DEFAULT_DISPLAY. - Preconditions.checkArgument(displayId == Display.DEFAULT_DISPLAY, - "Profile user can only be started in the default display"); - int parentUserId = getProfileParentId(userId); - Preconditions.checkArgument(parentUserId == currentUserId, - "Only profile of current user can be assigned to a display"); - if (DBG_MUMD) { - Slogf.d(LOG_TAG, "Ignoring profile user %d on default display", userId); - } - return; - } - - // Check if display is available - for (int i = 0; i < mUsersOnSecondaryDisplays.size(); i++) { - int assignedUserId = mUsersOnSecondaryDisplays.keyAt(i); - int assignedDisplayId = mUsersOnSecondaryDisplays.valueAt(i); - if (DBG_MUMD) { - Slogf.d(LOG_TAG, "%d: assignedUserId=%d, assignedDisplayId=%d", - i, assignedUserId, assignedDisplayId); - } - if (displayId == assignedDisplayId) { - throw new IllegalStateException("Cannot assign user " + userId + " to " - + "display " + displayId + " because such display is already " - + "assigned to user " + assignedUserId); - } - if (userId == assignedUserId) { - throw new IllegalStateException("Cannot assign user " + userId + " to " - + "display " + displayId + " because such user is as already " - + "assigned to display " + assignedDisplayId); - } - } - - if (DBG_MUMD) { - Slogf.d(LOG_TAG, "Adding full user %d -> display %d", userId, displayId); - } - mUsersOnSecondaryDisplays.put(userId, displayId); - } + public void assignUserToDisplay(@UserIdInt int userId, int displayId) { + mUserVisibilityMediator.assignUserToDisplay(userId, displayId); } @Override public void unassignUserFromDisplay(@UserIdInt int userId) { - if (DBG_MUMD) { - Slogf.d(LOG_TAG, "unassignUserFromDisplay(%d)", userId); - } - if (!mUsersOnSecondaryDisplaysEnabled) { - // Don't need to do anything because methods (such as isUserVisible()) already know - // that the current user (and their profiles) is assigned to the default display. - if (DBG_MUMD) { - Slogf.d(LOG_TAG, "ignoring when device doesn't support MUMD"); - } - return; - } - - synchronized (mUsersOnSecondaryDisplays) { - if (DBG_MUMD) { - Slogf.d(LOG_TAG, "Removing %d from mUsersOnSecondaryDisplays (%s)", userId, - mUsersOnSecondaryDisplays); - } - mUsersOnSecondaryDisplays.delete(userId); - } + mUserVisibilityMediator.unassignUserFromDisplay(userId); } @Override - public boolean isUserVisible(int userId) { - return isUserVisibleUnchecked(userId); + public boolean isUserVisible(@UserIdInt int userId) { + return mUserVisibilityMediator.isUserVisible(userId); } @Override - public boolean isUserVisible(int userId, int displayId) { - return isUserVisibleOnDisplay(userId, displayId); + public boolean isUserVisible(@UserIdInt int userId, int displayId) { + return mUserVisibilityMediator.isUserVisible(userId, displayId); } @Override - public int getDisplayAssignedToUser(int userId) { - return UserManagerService.this.getDisplayAssignedToUser(userId); + public int getDisplayAssignedToUser(@UserIdInt int userId) { + return mUserVisibilityMediator.getDisplayAssignedToUser(userId); } @Override - public int getUserAssignedToDisplay(int displayId) { - return UserManagerService.this.getUserAssignedToDisplay(displayId); + public @UserIdInt int getUserAssignedToDisplay(int displayId) { + return mUserVisibilityMediator.getUserAssignedToDisplay(displayId); } } // class LocalService diff --git a/services/core/java/com/android/server/pm/UserVisibilityMediator.java b/services/core/java/com/android/server/pm/UserVisibilityMediator.java new file mode 100644 index 000000000000..f725c486ec1f --- /dev/null +++ b/services/core/java/com/android/server/pm/UserVisibilityMediator.java @@ -0,0 +1,350 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.pm; + +import android.annotation.Nullable; +import android.annotation.UserIdInt; +import android.os.UserHandle; +import android.os.UserManager; +import android.util.IndentingPrintWriter; +import android.util.SparseIntArray; +import android.view.Display; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.Preconditions; +import com.android.server.utils.Slogf; + +import java.io.PrintWriter; + +/** + * Class responsible for deciding whether a user is visible (or visible for a given display). + * + * <p>This class is thread safe. + */ +// TODO(b/244644281): improve javadoc (for example, explain all cases / modes) +public final class UserVisibilityMediator { + + private static final boolean DBG = false; // DO NOT SUBMIT WITH TRUE + + private static final String TAG = UserVisibilityMediator.class.getSimpleName(); + + private final Object mLock = new Object(); + + // TODO(b/244644281): should not depend on service, but keep its own internal state (like + // current user and profile groups), but it is initially as the code was just moved from UMS + // "as is". Similarly, it shouldn't need to pass the SparseIntArray on constructor (which was + // added to UMS for testing purposes) + private final UserManagerService mService; + + private final boolean mUsersOnSecondaryDisplaysEnabled; + + @Nullable + @GuardedBy("mLock") + private final SparseIntArray mUsersOnSecondaryDisplays; + + UserVisibilityMediator(UserManagerService service) { + this(service, UserManager.isUsersOnSecondaryDisplaysEnabled(), + /* usersOnSecondaryDisplays= */ null); + } + + @VisibleForTesting + UserVisibilityMediator(UserManagerService service, boolean usersOnSecondaryDisplaysEnabled, + @Nullable SparseIntArray usersOnSecondaryDisplays) { + mService = service; + mUsersOnSecondaryDisplaysEnabled = usersOnSecondaryDisplaysEnabled; + if (mUsersOnSecondaryDisplaysEnabled) { + mUsersOnSecondaryDisplays = usersOnSecondaryDisplays == null + ? new SparseIntArray() // default behavior + : usersOnSecondaryDisplays; // passed by unit test + } else { + mUsersOnSecondaryDisplays = null; + } + } + + /** + * See {@link UserManagerInternal#assignUserToDisplay(int, int)}. + */ + public void assignUserToDisplay(int userId, int displayId) { + if (DBG) { + Slogf.d(TAG, "assignUserToDisplay(%d, %d)", userId, displayId); + } + + // NOTE: Using Boolean instead of boolean as it will be re-used below + Boolean isProfile = null; + if (displayId == Display.DEFAULT_DISPLAY) { + if (mUsersOnSecondaryDisplaysEnabled) { + // Profiles are only supported in the default display, but it cannot return yet + // as it needs to check if the parent is also assigned to the DEFAULT_DISPLAY + // (this is done indirectly below when it checks that the profile parent is the + // current user, as the current user is always assigned to the DEFAULT_DISPLAY). + isProfile = isProfileUnchecked(userId); + } + if (isProfile == null || !isProfile) { + // Don't need to do anything because methods (such as isUserVisible()) already + // know that the current user (and their profiles) is assigned to the default + // display. + if (DBG) { + Slogf.d(TAG, "ignoring on default display"); + } + return; + } + } + + if (!mUsersOnSecondaryDisplaysEnabled) { + throw new UnsupportedOperationException("assignUserToDisplay(" + userId + ", " + + displayId + ") called on device that doesn't support multiple " + + "users on multiple displays"); + } + + Preconditions.checkArgument(userId != UserHandle.USER_SYSTEM, "Cannot assign system " + + "user to secondary display (%d)", displayId); + Preconditions.checkArgument(displayId != Display.INVALID_DISPLAY, + "Cannot assign to INVALID_DISPLAY (%d)", displayId); + + int currentUserId = getCurrentUserId(); + Preconditions.checkArgument(userId != currentUserId, + "Cannot assign current user (%d) to other displays", currentUserId); + + if (isProfile == null) { + isProfile = isProfileUnchecked(userId); + } + synchronized (mLock) { + if (isProfile) { + // Profile can only start in the same display as parent. And for simplicity, + // that display must be the DEFAULT_DISPLAY. + Preconditions.checkArgument(displayId == Display.DEFAULT_DISPLAY, + "Profile user can only be started in the default display"); + int parentUserId = getProfileParentId(userId); + Preconditions.checkArgument(parentUserId == currentUserId, + "Only profile of current user can be assigned to a display"); + if (DBG) { + Slogf.d(TAG, "Ignoring profile user %d on default display", userId); + } + return; + } + + // Check if display is available + for (int i = 0; i < mUsersOnSecondaryDisplays.size(); i++) { + int assignedUserId = mUsersOnSecondaryDisplays.keyAt(i); + int assignedDisplayId = mUsersOnSecondaryDisplays.valueAt(i); + if (DBG) { + Slogf.d(TAG, "%d: assignedUserId=%d, assignedDisplayId=%d", + i, assignedUserId, assignedDisplayId); + } + if (displayId == assignedDisplayId) { + throw new IllegalStateException("Cannot assign user " + userId + " to " + + "display " + displayId + " because such display is already " + + "assigned to user " + assignedUserId); + } + if (userId == assignedUserId) { + throw new IllegalStateException("Cannot assign user " + userId + " to " + + "display " + displayId + " because such user is as already " + + "assigned to display " + assignedDisplayId); + } + } + + if (DBG) { + Slogf.d(TAG, "Adding full user %d -> display %d", userId, displayId); + } + mUsersOnSecondaryDisplays.put(userId, displayId); + } + } + + /** + * See {@link UserManagerInternal#unassignUserFromDisplay(int)}. + */ + public void unassignUserFromDisplay(int userId) { + if (DBG) { + Slogf.d(TAG, "unassignUserFromDisplay(%d)", userId); + } + if (!mUsersOnSecondaryDisplaysEnabled) { + // Don't need to do anything because methods (such as isUserVisible()) already know + // that the current user (and their profiles) is assigned to the default display. + if (DBG) { + Slogf.d(TAG, "ignoring when device doesn't support MUMD"); + } + return; + } + + synchronized (mLock) { + if (DBG) { + Slogf.d(TAG, "Removing %d from mUsersOnSecondaryDisplays (%s)", userId, + mUsersOnSecondaryDisplays); + } + mUsersOnSecondaryDisplays.delete(userId); + } + } + + /** + * See {@link UserManagerInternal#isUserVisible(int)}. + */ + public boolean isUserVisible(int userId) { + // First check current foreground user and their profiles (on main display) + if (isCurrentUserOrRunningProfileOfCurrentUser(userId)) { + return true; + } + + // Device doesn't support multiple users on multiple displays, so only users checked above + // can be visible + if (!mUsersOnSecondaryDisplaysEnabled) { + return false; + } + + synchronized (mLock) { + return mUsersOnSecondaryDisplays.indexOfKey(userId) >= 0; + } + } + + /** + * See {@link UserManagerInternal#isUserVisible(int, int)}. + */ + public boolean isUserVisible(int userId, int displayId) { + if (displayId == Display.INVALID_DISPLAY) { + return false; + } + if (!mUsersOnSecondaryDisplaysEnabled) { + return isCurrentUserOrRunningProfileOfCurrentUser(userId); + } + + // TODO(b/244644281): temporary workaround to let WM use this API without breaking current + // behavior - return true for current user / profile for any display (other than those + // explicitly assigned to another users), otherwise they wouldn't be able to launch + // activities on other non-passenger displays, like cluster, display, or virtual displays). + // In the long-term, it should rely just on mUsersOnSecondaryDisplays, which + // would be updated by DisplayManagerService when displays are created / initialized. + if (isCurrentUserOrRunningProfileOfCurrentUser(userId)) { + synchronized (mLock) { + boolean assignedToUser = false; + boolean assignedToAnotherUser = false; + for (int i = 0; i < mUsersOnSecondaryDisplays.size(); i++) { + if (mUsersOnSecondaryDisplays.valueAt(i) == displayId) { + if (mUsersOnSecondaryDisplays.keyAt(i) == userId) { + assignedToUser = true; + break; + } else { + assignedToAnotherUser = true; + // Cannot break because it could be assigned to a profile of the user + // (and we better not assume that the iteration will check for the + // parent user before its profiles) + } + } + } + if (DBG) { + Slogf.d(TAG, "isUserVisibleOnDisplay(%d, %d): assignedToUser=%b, " + + "assignedToAnotherUser=%b, mUsersOnSecondaryDisplays=%s", + userId, displayId, assignedToUser, assignedToAnotherUser, + mUsersOnSecondaryDisplays); + } + return assignedToUser || !assignedToAnotherUser; + } + } + + synchronized (mLock) { + return mUsersOnSecondaryDisplays.get(userId, Display.INVALID_DISPLAY) == displayId; + } + } + + /** + * See {@link UserManagerInternal#getDisplayAssignedToUser(int)}. + */ + public int getDisplayAssignedToUser(int userId) { + if (isCurrentUserOrRunningProfileOfCurrentUser(userId)) { + return Display.DEFAULT_DISPLAY; + } + + if (!mUsersOnSecondaryDisplaysEnabled) { + return Display.INVALID_DISPLAY; + } + + synchronized (mLock) { + return mUsersOnSecondaryDisplays.get(userId, Display.INVALID_DISPLAY); + } + } + + /** + * See {@link UserManagerInternal#getUserAssignedToDisplay(int)}. + */ + public int getUserAssignedToDisplay(int displayId) { + if (displayId == Display.DEFAULT_DISPLAY || !mUsersOnSecondaryDisplaysEnabled) { + return getCurrentUserId(); + } + + synchronized (mLock) { + for (int i = 0; i < mUsersOnSecondaryDisplays.size(); i++) { + if (mUsersOnSecondaryDisplays.valueAt(i) != displayId) { + continue; + } + int userId = mUsersOnSecondaryDisplays.keyAt(i); + if (!isProfileUnchecked(userId)) { + return userId; + } else if (DBG) { + Slogf.d(TAG, "getUserAssignedToDisplay(%d): skipping user %d because it's " + + "a profile", displayId, userId); + } + } + } + + int currentUserId = getCurrentUserId(); + if (DBG) { + Slogf.d(TAG, "getUserAssignedToDisplay(%d): no user assigned to display, returning " + + "current user (%d) instead", displayId, currentUserId); + } + return currentUserId; + } + + private void dump(IndentingPrintWriter ipw) { + ipw.println("UserVisibilityManager"); + ipw.increaseIndent(); + + ipw.print("Supports users on secondary displays: "); + ipw.println(mUsersOnSecondaryDisplaysEnabled); + + if (mUsersOnSecondaryDisplaysEnabled) { + ipw.print("Users on secondary displays: "); + synchronized (mLock) { + ipw.println(mUsersOnSecondaryDisplays); + } + } + + ipw.decreaseIndent(); + } + + void dump(PrintWriter pw) { + if (pw instanceof IndentingPrintWriter) { + dump((IndentingPrintWriter) pw); + return; + } + dump(new IndentingPrintWriter(pw)); + } + + // TODO(b/244644281): remove methods below once this class caches that state + private @UserIdInt int getCurrentUserId() { + return mService.getCurrentUserId(); + } + + private boolean isCurrentUserOrRunningProfileOfCurrentUser(@UserIdInt int userId) { + return mService.isCurrentUserOrRunningProfileOfCurrentUser(userId); + } + + private boolean isProfileUnchecked(@UserIdInt int userId) { + return mService.isProfileUnchecked(userId); + } + + private @UserIdInt int getProfileParentId(@UserIdInt int userId) { + return mService.getProfileParentId(userId); + } +} diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java index 799ef41f3067..ab223ef3cbeb 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java +++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java @@ -4245,7 +4245,6 @@ public class PermissionManagerServiceImpl implements PermissionManagerServiceInt } boolean changed = false; - Set<Permission> needsUpdate = null; synchronized (mLock) { final Iterator<Permission> it = mRegistry.getPermissionTrees().iterator(); while (it.hasNext()) { @@ -4264,26 +4263,6 @@ public class PermissionManagerServiceImpl implements PermissionManagerServiceInt + " that used to be declared by " + bp.getPackageName()); it.remove(); } - if (needsUpdate == null) { - needsUpdate = new ArraySet<>(); - } - needsUpdate.add(bp); - } - } - if (needsUpdate != null) { - for (final Permission bp : needsUpdate) { - final AndroidPackage sourcePkg = - mPackageManagerInt.getPackage(bp.getPackageName()); - final PackageStateInternal sourcePs = - mPackageManagerInt.getPackageStateInternal(bp.getPackageName()); - synchronized (mLock) { - if (sourcePkg != null && sourcePs != null) { - continue; - } - Slog.w(TAG, "Removing dangling permission tree: " + bp.getName() - + " from package " + bp.getPackageName()); - mRegistry.removePermission(bp.getName()); - } } } return changed; diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index a6fac4d60fe7..98b5c1ba639b 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -256,6 +256,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { static final int SHORT_PRESS_POWER_GO_HOME = 4; static final int SHORT_PRESS_POWER_CLOSE_IME_OR_GO_HOME = 5; static final int SHORT_PRESS_POWER_LOCK_OR_SLEEP = 6; + static final int SHORT_PRESS_POWER_DREAM_OR_SLEEP = 7; // must match: config_LongPressOnPowerBehavior in config.xml static final int LONG_PRESS_POWER_NOTHING = 0; @@ -969,7 +970,14 @@ public class PhoneWindowManager implements WindowManagerPolicy { powerMultiPressAction(eventTime, interactive, mTriplePressOnPowerBehavior); } else if (count > 3 && count <= getMaxMultiPressPowerCount()) { Slog.d(TAG, "No behavior defined for power press count " + count); - } else if (count == 1 && interactive && !beganFromNonInteractive) { + } else if (count == 1 && interactive) { + if (beganFromNonInteractive) { + // The "screen is off" case, where we might want to start dreaming on power button + // press. + attemptToDreamFromShortPowerButtonPress(false, () -> {}); + return; + } + if (mSideFpsEventHandler.shouldConsumeSinglePress(eventTime)) { Slog.i(TAG, "Suppressing power key because the user is interacting with the " + "fingerprint sensor"); @@ -1018,11 +1026,39 @@ public class PhoneWindowManager implements WindowManagerPolicy { } break; } + case SHORT_PRESS_POWER_DREAM_OR_SLEEP: { + attemptToDreamFromShortPowerButtonPress( + true, + () -> sleepDefaultDisplayFromPowerButton(eventTime, 0)); + break; + } } } } /** + * Attempt to dream from a power button press. + * + * @param isScreenOn Whether the screen is currently on. + * @param noDreamAction The action to perform if dreaming is not possible. + */ + private void attemptToDreamFromShortPowerButtonPress( + boolean isScreenOn, Runnable noDreamAction) { + if (mShortPressOnPowerBehavior != SHORT_PRESS_POWER_DREAM_OR_SLEEP) { + noDreamAction.run(); + return; + } + + final DreamManagerInternal dreamManagerInternal = getDreamManagerInternal(); + if (dreamManagerInternal == null || !dreamManagerInternal.canStartDreaming(isScreenOn)) { + noDreamAction.run(); + return; + } + + dreamManagerInternal.requestDream(); + } + + /** * Sends the default display to sleep as a result of a power button press. * * @return {@code true} if the device was sent to sleep, {@code false} if the device did not @@ -1593,7 +1629,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { // If there's a dream running then use home to escape the dream // but don't actually go home. - if (mDreamManagerInternal != null && mDreamManagerInternal.isDreaming()) { + final DreamManagerInternal dreamManagerInternal = getDreamManagerInternal(); + if (dreamManagerInternal != null && dreamManagerInternal.isDreaming()) { mDreamManagerInternal.stopDream(false /*immediate*/, "short press on home" /*reason*/); return; } @@ -2529,6 +2566,15 @@ public class PhoneWindowManager implements WindowManagerPolicy { } } + private DreamManagerInternal getDreamManagerInternal() { + if (mDreamManagerInternal == null) { + // If mDreamManagerInternal is null, attempt to re-fetch it. + mDreamManagerInternal = LocalServices.getService(DreamManagerInternal.class); + } + + return mDreamManagerInternal; + } + private void updateWakeGestureListenerLp() { if (shouldEnableWakeGestureLp()) { mWakeGestureListener.requestWakeUpTrigger(); @@ -4131,6 +4177,9 @@ public class PhoneWindowManager implements WindowManagerPolicy { case KeyEvent.KEYCODE_DEMO_APP_2: case KeyEvent.KEYCODE_DEMO_APP_3: case KeyEvent.KEYCODE_DEMO_APP_4: { + // TODO(b/254604589): Dispatch KeyEvent to System UI. + sendSystemKeyToStatusBarAsync(keyCode); + // Just drop if keys are not intercepted for direct key. result &= ~ACTION_PASS_TO_USER; break; diff --git a/services/core/java/com/android/server/utils/EventLogger.java b/services/core/java/com/android/server/utils/EventLogger.java index 004312f06119..11766a3d70bd 100644 --- a/services/core/java/com/android/server/utils/EventLogger.java +++ b/services/core/java/com/android/server/utils/EventLogger.java @@ -43,7 +43,7 @@ public class EventLogger { /** * The maximum number of events to keep in {@code mEvents}. * - * <p>Calling {@link #log} when the size of {@link #mEvents} matches the threshold will + * <p>Calling {@link #enqueue} when the size of {@link #mEvents} matches the threshold will * cause the oldest event to be evicted. */ private final int mMemSize; @@ -60,7 +60,7 @@ public class EventLogger { } /** Enqueues {@code event} to be logged. */ - public synchronized void log(Event event) { + public synchronized void enqueue(Event event) { if (mEvents.size() >= mMemSize) { mEvents.removeLast(); } @@ -69,24 +69,14 @@ public class EventLogger { } /** - * Add a string-based event to the log, and print it to logcat as info. - * @param msg the message for the logs - * @param tag the logcat tag to use - */ - public synchronized void loglogi(String msg, String tag) { - final Event event = new StringEvent(msg); - log(event.printLog(tag)); - } - - /** - * Same as {@link #loglogi(String, String)} but specifying the logcat type + * Add a string-based event to the log, and print it to logcat with a specific severity. * @param msg the message for the logs * @param logType the type of logcat entry * @param tag the logcat tag to use */ - public synchronized void loglog(String msg, @Event.LogType int logType, String tag) { + public synchronized void enqueueAndLog(String msg, @Event.LogType int logType, String tag) { final Event event = new StringEvent(msg); - log(event.printLog(logType, tag)); + enqueue(event.printLog(logType, tag)); } /** Dumps events using {@link PrintWriter}. */ diff --git a/services/core/java/com/android/server/wm/ActivityStartInterceptor.java b/services/core/java/com/android/server/wm/ActivityStartInterceptor.java index 7d84bdf78056..d7c5e9373ad3 100644 --- a/services/core/java/com/android/server/wm/ActivityStartInterceptor.java +++ b/services/core/java/com/android/server/wm/ActivityStartInterceptor.java @@ -424,7 +424,7 @@ class ActivityStartInterceptor { try { harmfulAppWarning = mService.getPackageManager() .getHarmfulAppWarning(mAInfo.packageName, mUserId); - } catch (RemoteException ex) { + } catch (RemoteException | IllegalArgumentException ex) { return false; } diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index ecc43f7b938b..b153a85a4048 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -1316,7 +1316,7 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { mAppSwitchesState = APP_SWITCH_ALLOW; } } - return pir.sendInner(0, fillInIntent, resolvedType, allowlistToken, null, null, + return pir.sendInner(caller, 0, fillInIntent, resolvedType, allowlistToken, null, null, resultTo, resultWho, requestCode, flagsMask, flagsValues, bOptions); } diff --git a/services/core/java/com/android/server/wm/AppTransition.java b/services/core/java/com/android/server/wm/AppTransition.java index d2c098b73e71..a487797ad1f3 100644 --- a/services/core/java/com/android/server/wm/AppTransition.java +++ b/services/core/java/com/android/server/wm/AppTransition.java @@ -1448,6 +1448,12 @@ public class AppTransition implements Dump { || transit == TRANSIT_OLD_ACTIVITY_RELAUNCH; } + static boolean isTaskFragmentTransitOld(@TransitionOldType int transit) { + return transit == TRANSIT_OLD_TASK_FRAGMENT_OPEN + || transit == TRANSIT_OLD_TASK_FRAGMENT_CLOSE + || transit == TRANSIT_OLD_TASK_FRAGMENT_CHANGE; + } + static boolean isChangeTransitOld(@TransitionOldType int transit) { return transit == TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE || transit == TRANSIT_OLD_TASK_FRAGMENT_CHANGE; diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 739f41f170aa..8b3444318636 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -47,7 +47,6 @@ import static android.view.Display.STATE_UNKNOWN; import static android.view.Display.isSuspendedState; import static android.view.InsetsState.ITYPE_IME; import static android.view.InsetsState.ITYPE_LEFT_GESTURES; -import static android.view.InsetsState.ITYPE_NAVIGATION_BAR; import static android.view.InsetsState.ITYPE_RIGHT_GESTURES; import static android.view.Surface.ROTATION_0; import static android.view.Surface.ROTATION_270; @@ -55,6 +54,7 @@ import static android.view.Surface.ROTATION_90; import static android.view.View.GONE; import static android.view.WindowInsets.Type.displayCutout; import static android.view.WindowInsets.Type.ime; +import static android.view.WindowInsets.Type.navigationBars; import static android.view.WindowInsets.Type.systemBars; import static android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE; import static android.view.WindowManager.DISPLAY_IME_POLICY_FALLBACK_DISPLAY; @@ -216,7 +216,6 @@ import android.view.InputDevice; import android.view.InsetsSource; import android.view.InsetsState; import android.view.InsetsState.InternalInsetsType; -import android.view.InsetsVisibilities; import android.view.MagnificationSpec; import android.view.PrivacyIndicatorBounds; import android.view.RemoteAnimationDefinition; @@ -227,6 +226,7 @@ import android.view.SurfaceControl; import android.view.SurfaceControl.Transaction; import android.view.SurfaceSession; import android.view.WindowInsets; +import android.view.WindowInsets.Type.InsetsType; import android.view.WindowManager; import android.view.WindowManager.DisplayImePolicy; import android.view.WindowManagerPolicyConstants.PointerEventListener; @@ -788,11 +788,11 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp // higher window hierarchy, we don't give it focus if the next IME layering target // doesn't request IME visible. if (w.mIsImWindow && w.isChildWindow() && (mImeLayeringTarget == null - || !mImeLayeringTarget.getRequestedVisibility(ITYPE_IME))) { + || !mImeLayeringTarget.isRequestedVisible(ime()))) { return false; } if (w.mAttrs.type == TYPE_INPUT_METHOD_DIALOG && mImeLayeringTarget != null - && !mImeLayeringTarget.getRequestedVisibility(ITYPE_IME) + && !mImeLayeringTarget.isRequestedVisible(ime()) && !mImeLayeringTarget.isVisibleRequested()) { return false; } @@ -2059,7 +2059,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp // is opened for logging metrics. if (mWmService.mAccessibilityController.hasCallbacks()) { final boolean isImeShow = mImeControlTarget != null - && mImeControlTarget.getRequestedVisibility(ITYPE_IME); + && mImeControlTarget.isRequestedVisible(ime()); mWmService.mAccessibilityController.updateImeVisibilityIfNeeded(mDisplayId, isImeShow); } } @@ -5662,7 +5662,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp final int type = win.mAttrs.type; final int privateFlags = win.mAttrs.privateFlags; final boolean stickyHideNav = - !win.getRequestedVisibility(ITYPE_NAVIGATION_BAR) + !win.isRequestedVisible(navigationBars()) && win.mAttrs.insetsFlags.behavior == BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE; return (!stickyHideNav || ignoreRequest) && type != TYPE_INPUT_METHOD && type != TYPE_NOTIFICATION_SHADE && win.getActivityType() != ACTIVITY_TYPE_HOME @@ -6672,7 +6672,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp class RemoteInsetsControlTarget implements InsetsControlTarget { private final IDisplayWindowInsetsController mRemoteInsetsController; - private final InsetsVisibilities mRequestedVisibilities = new InsetsVisibilities(); + private @InsetsType int mRequestedVisibleTypes = WindowInsets.Type.defaultVisible(); private final boolean mCanShowTransient; RemoteInsetsControlTarget(IDisplayWindowInsetsController controller) { @@ -6685,12 +6685,12 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp * Notifies the remote insets controller that the top focused window has changed. * * @param component The application component that is open in the top focussed window. - * @param requestedVisibilities The insets visibilities requested by the focussed window. + * @param requestedVisibleTypes The insets types requested visible by the focused window. */ void topFocusedWindowChanged(ComponentName component, - InsetsVisibilities requestedVisibilities) { + @InsetsType int requestedVisibleTypes) { try { - mRemoteInsetsController.topFocusedWindowChanged(component, requestedVisibilities); + mRemoteInsetsController.topFocusedWindowChanged(component, requestedVisibleTypes); } catch (RemoteException e) { Slog.w(TAG, "Failed to deliver package in top focused window change", e); } @@ -6726,7 +6726,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp } @Override - public void hideInsets(@WindowInsets.Type.InsetsType int types, boolean fromIme) { + public void hideInsets(@InsetsType int types, boolean fromIme) { try { mRemoteInsetsController.hideInsets(types, fromIme); } catch (RemoteException e) { @@ -6740,15 +6740,25 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp } @Override - public boolean getRequestedVisibility(@InternalInsetsType int type) { - if (type == ITYPE_IME) { + public boolean isRequestedVisible(@InsetsType int types) { + if (types == ime()) { return getInsetsStateController().getImeSourceProvider().isImeShowing(); } - return mRequestedVisibilities.getVisibility(type); + return (mRequestedVisibleTypes & types) != 0; } - void setRequestedVisibilities(InsetsVisibilities requestedVisibilities) { - mRequestedVisibilities.set(requestedVisibilities); + @Override + public @InsetsType int getRequestedVisibleTypes() { + return mRequestedVisibleTypes; + } + + /** + * @see #getRequestedVisibleTypes() + */ + void setRequestedVisibleTypes(@InsetsType int requestedVisibleTypes) { + if (mRequestedVisibleTypes != requestedVisibleTypes) { + mRequestedVisibleTypes = requestedVisibleTypes; + } } } diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java index 442777a20281..87239944fa60 100644 --- a/services/core/java/com/android/server/wm/DisplayPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayPolicy.java @@ -19,14 +19,11 @@ package com.android.server.wm; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.view.Display.TYPE_INTERNAL; -import static android.view.InsetsState.ITYPE_BOTTOM_MANDATORY_GESTURES; -import static android.view.InsetsState.ITYPE_BOTTOM_TAPPABLE_ELEMENT; +import static android.view.InsetsFrameProvider.SOURCE_FRAME; import static android.view.InsetsState.ITYPE_CAPTION_BAR; import static android.view.InsetsState.ITYPE_CLIMATE_BAR; import static android.view.InsetsState.ITYPE_EXTRA_NAVIGATION_BAR; -import static android.view.InsetsState.ITYPE_LEFT_GESTURES; import static android.view.InsetsState.ITYPE_NAVIGATION_BAR; -import static android.view.InsetsState.ITYPE_RIGHT_GESTURES; import static android.view.InsetsState.ITYPE_STATUS_BAR; import static android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS; import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS; @@ -47,7 +44,6 @@ import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_INTERCEPT_GLO import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_UNRESTRICTED_GESTURE_EXCLUSION; import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; -import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD; import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR; import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL; import static android.view.WindowManager.LayoutParams.TYPE_NOTIFICATION_SHADE; @@ -127,6 +123,7 @@ import android.view.InsetsVisibilities; import android.view.Surface; import android.view.View; import android.view.ViewDebug; +import android.view.WindowInsets; import android.view.WindowInsets.Type; import android.view.WindowInsets.Type.InsetsType; import android.view.WindowLayout; @@ -209,14 +206,11 @@ public class DisplayPolicy { private StatusBarManagerInternal mStatusBarManagerInternal; @Px - private int mBottomGestureAdditionalInset; - @Px private int mLeftGestureInset; @Px private int mRightGestureInset; private boolean mCanSystemBarsBeShownByUser; - private boolean mNavButtonForcedVisible; StatusBarManagerInternal getStatusBarManagerInternal() { synchronized (mServiceAcquireLock) { @@ -240,7 +234,6 @@ public class DisplayPolicy { private volatile boolean mHasNavigationBar; // Can the navigation bar ever move to the side? private volatile boolean mNavigationBarCanMove; - private volatile boolean mNavigationBarLetsThroughTaps; private volatile boolean mNavigationBarAlwaysShowOnSideGesture; // Written by vr manager thread, only read in this class. @@ -324,6 +317,7 @@ public class DisplayPolicy { private int mLastDisableFlags; private int mLastAppearance; private int mLastBehavior; + private int mLastRequestedVisibleTypes = Type.defaultVisible(); private InsetsVisibilities mRequestedVisibilities = new InsetsVisibilities(); private AppearanceRegion[] mLastStatusBarAppearanceRegions; private LetterboxDetails[] mLastLetterboxDetails; @@ -360,8 +354,6 @@ public class DisplayPolicy { private PointerLocationView mPointerLocationView; - private int mDisplayCutoutTouchableRegionSize; - private RefreshRatePolicy mRefreshRatePolicy; /** @@ -1150,71 +1142,9 @@ public class DisplayPolicy { break; case TYPE_NAVIGATION_BAR: mNavigationBar = win; - final TriConsumer<DisplayFrames, WindowContainer, Rect> navFrameProvider = - (displayFrames, windowContainer, inOutFrame) -> { - if (!mNavButtonForcedVisible) { - final LayoutParams lp = - win.mAttrs.forRotation(displayFrames.mRotation); - if (lp.providedInsets != null) { - for (InsetsFrameProvider provider : lp.providedInsets) { - if (provider.type != ITYPE_NAVIGATION_BAR) { - continue; - } - InsetsFrameProvider.calculateInsetsFrame( - displayFrames.mUnrestricted, - win.getBounds(), displayFrames.mDisplayCutoutSafe, - inOutFrame, provider.source, - provider.insetsSize, lp.privateFlags, - provider.minimalInsetsSizeInDisplayCutoutSafe); - } - } - inOutFrame.inset(win.mGivenContentInsets); - } - }; - final SparseArray<TriConsumer<DisplayFrames, WindowContainer, Rect>> imeOverride = - new SparseArray<>(); - // For IME, we don't modify the frame. - imeOverride.put(TYPE_INPUT_METHOD, null); - mDisplayContent.setInsetProvider(ITYPE_NAVIGATION_BAR, win, - navFrameProvider, imeOverride); - - mDisplayContent.setInsetProvider(ITYPE_BOTTOM_MANDATORY_GESTURES, win, - (displayFrames, windowContainer, inOutFrame) -> { - inOutFrame.top -= mBottomGestureAdditionalInset; - }); - mDisplayContent.setInsetProvider(ITYPE_LEFT_GESTURES, win, - (displayFrames, windowContainer, inOutFrame) -> { - final int leftSafeInset = - Math.max(displayFrames.mDisplayCutoutSafe.left, 0); - inOutFrame.left = 0; - inOutFrame.top = 0; - inOutFrame.bottom = displayFrames.mHeight; - inOutFrame.right = leftSafeInset + mLeftGestureInset; - }); - mDisplayContent.setInsetProvider(ITYPE_RIGHT_GESTURES, win, - (displayFrames, windowContainer, inOutFrame) -> { - final int rightSafeInset = - Math.min(displayFrames.mDisplayCutoutSafe.right, - displayFrames.mUnrestricted.right); - inOutFrame.left = rightSafeInset - mRightGestureInset; - inOutFrame.top = 0; - inOutFrame.bottom = displayFrames.mHeight; - inOutFrame.right = displayFrames.mWidth; - }); - mDisplayContent.setInsetProvider(ITYPE_BOTTOM_TAPPABLE_ELEMENT, win, - (displayFrames, windowContainer, inOutFrame) -> { - if ((win.getAttrs().flags & FLAG_NOT_TOUCHABLE) != 0 - || mNavigationBarLetsThroughTaps) { - inOutFrame.setEmpty(); - } - }); - mInsetsSourceWindowsExceptIme.add(win); - if (DEBUG_LAYOUT) Slog.i(TAG, "NAVIGATION BAR: " + mNavigationBar); break; } - // TODO(b/239145252): Temporarily skip the navigation bar as it is still with the hard-coded - // logic. - if (attrs.providedInsets != null && attrs.type != TYPE_NAVIGATION_BAR) { + if (attrs.providedInsets != null) { for (int i = attrs.providedInsets.length - 1; i >= 0; i--) { final InsetsFrameProvider provider = attrs.providedInsets[i]; switch (provider.type) { @@ -1242,24 +1172,8 @@ public class DisplayPolicy { // The index of the provider and corresponding insets types cannot change at // runtime as ensured in WMS. Make use of the index in the provider directly // to access the latest provided size at runtime. - final int index = i; final TriConsumer<DisplayFrames, WindowContainer, Rect> frameProvider = - provider.insetsSize != null - ? (displayFrames, windowContainer, inOutFrame) -> { - inOutFrame.inset(win.mGivenContentInsets); - final LayoutParams lp = - win.mAttrs.forRotation(displayFrames.mRotation); - final InsetsFrameProvider ifp = - win.mAttrs.forRotation(displayFrames.mRotation) - .providedInsets[index]; - InsetsFrameProvider.calculateInsetsFrame( - displayFrames.mUnrestricted, - windowContainer.getBounds(), - displayFrames.mDisplayCutoutSafe, - inOutFrame, ifp.source, - ifp.insetsSize, lp.privateFlags, - ifp.minimalInsetsSizeInDisplayCutoutSafe); - } : null; + getFrameProvider(win, provider, i); final InsetsFrameProvider.InsetsSizeOverride[] overrides = provider.insetsSizeOverrides; final SparseArray<TriConsumer<DisplayFrames, WindowContainer, Rect>> @@ -1267,27 +1181,10 @@ public class DisplayPolicy { if (overrides != null) { overrideProviders = new SparseArray<>(); for (int j = overrides.length - 1; j >= 0; j--) { - final int overrideIndex = j; final TriConsumer<DisplayFrames, WindowContainer, Rect> overrideFrameProvider = - (displayFrames, windowContainer, inOutFrame) -> { - final LayoutParams lp = - win.mAttrs.forRotation( - displayFrames.mRotation); - final InsetsFrameProvider ifp = - win.mAttrs.providedInsets[index]; - InsetsFrameProvider.calculateInsetsFrame( - displayFrames.mUnrestricted, - windowContainer.getBounds(), - displayFrames.mDisplayCutoutSafe, - inOutFrame, ifp.source, - ifp.insetsSizeOverrides[ - overrideIndex].insetsSize, - lp.privateFlags, - null); - }; - overrideProviders.put(overrides[j].windowType, - overrideFrameProvider); + getOverrideFrameProvider(win, i, j); + overrideProviders.put(overrides[j].windowType, overrideFrameProvider); } } else { overrideProviders = null; @@ -1299,6 +1196,36 @@ public class DisplayPolicy { } } + @Nullable + private TriConsumer<DisplayFrames, WindowContainer, Rect> getFrameProvider(WindowState win, + InsetsFrameProvider provider, int index) { + if (provider.insetsSize == null && provider.source == SOURCE_FRAME) { + return null; + } + return (displayFrames, windowContainer, inOutFrame) -> { + inOutFrame.inset(win.mGivenContentInsets); + final LayoutParams lp = win.mAttrs.forRotation(displayFrames.mRotation); + final InsetsFrameProvider ifp = lp.providedInsets[index]; + InsetsFrameProvider.calculateInsetsFrame(displayFrames.mUnrestricted, + windowContainer.getBounds(), displayFrames.mDisplayCutoutSafe, inOutFrame, + ifp.source, ifp.insetsSize, lp.privateFlags, + ifp.minimalInsetsSizeInDisplayCutoutSafe); + }; + } + + @NonNull + private TriConsumer<DisplayFrames, WindowContainer, Rect> getOverrideFrameProvider( + WindowState win, int index, int overrideIndex) { + return (displayFrames, windowContainer, inOutFrame) -> { + final LayoutParams lp = win.mAttrs.forRotation(displayFrames.mRotation); + final InsetsFrameProvider ifp = lp.providedInsets[index]; + InsetsFrameProvider.calculateInsetsFrame(displayFrames.mUnrestricted, + windowContainer.getBounds(), displayFrames.mDisplayCutoutSafe, inOutFrame, + ifp.source, ifp.insetsSizeOverrides[overrideIndex].insetsSize, lp.privateFlags, + null); + }; + } + @WindowManagerPolicy.AltBarPosition private int getAltBarPosition(WindowManager.LayoutParams params) { switch (params.gravity) { @@ -1386,16 +1313,6 @@ public class DisplayPolicy { mInsetsSourceWindowsExceptIme.remove(win); } - private int getStatusBarHeight(DisplayFrames displayFrames) { - int statusBarHeight; - if (mStatusBar != null) { - statusBarHeight = mStatusBar.mAttrs.forRotation(displayFrames.mRotation).height; - } else { - statusBarHeight = 0; - } - return Math.max(statusBarHeight, displayFrames.mDisplayCutoutSafe.top); - } - WindowState getStatusBar() { return mStatusBar != null ? mStatusBar : mStatusBarAlt; } @@ -1551,7 +1468,7 @@ public class DisplayPolicy { mWindowLayout.computeFrames(win.mAttrs.forRotation(displayFrames.mRotation), displayFrames.mInsetsState, displayFrames.mDisplayCutoutSafe, displayFrames.mUnrestricted, win.getWindowingMode(), UNSPECIFIED_LENGTH, - UNSPECIFIED_LENGTH, win.getRequestedVisibilities(), win.mGlobalScale, + UNSPECIFIED_LENGTH, win.getRequestedVisibleTypes(), win.mGlobalScale, sTmpClientFrames); final SparseArray<InsetsSource> sources = win.getProvidedInsetsSources(); final InsetsState state = displayFrames.mInsetsState; @@ -1598,7 +1515,7 @@ public class DisplayPolicy { mWindowLayout.computeFrames(attrs, win.getInsetsState(), displayFrames.mDisplayCutoutSafe, win.getBounds(), win.getWindowingMode(), requestedWidth, requestedHeight, - win.getRequestedVisibilities(), win.mGlobalScale, sTmpClientFrames); + win.getRequestedVisibleTypes(), win.mGlobalScale, sTmpClientFrames); win.setFrames(sTmpClientFrames, win.mRequestedWidth, win.mRequestedHeight); } @@ -1861,7 +1778,7 @@ public class DisplayPolicy { if (mTopFullscreenOpaqueWindowState == null || mForceShowSystemBars) { return false; } - return !mTopFullscreenOpaqueWindowState.getRequestedVisibility(ITYPE_STATUS_BAR); + return !mTopFullscreenOpaqueWindowState.isRequestedVisible(Type.statusBars()); } /** @@ -1892,27 +1809,12 @@ public class DisplayPolicy { final Resources res = getCurrentUserResources(); final int portraitRotation = displayRotation.getPortraitRotation(); - if (hasStatusBar()) { - mDisplayCutoutTouchableRegionSize = res.getDimensionPixelSize( - R.dimen.display_cutout_touchable_region_size); - } else { - mDisplayCutoutTouchableRegionSize = 0; - } - mNavBarOpacityMode = res.getInteger(R.integer.config_navBarOpacityMode); mLeftGestureInset = mGestureNavigationSettingsObserver.getLeftSensitivity(res); mRightGestureInset = mGestureNavigationSettingsObserver.getRightSensitivity(res); - mNavButtonForcedVisible = - mGestureNavigationSettingsObserver.areNavigationButtonForcedVisible(); - mNavigationBarLetsThroughTaps = res.getBoolean(R.bool.config_navBarTapThrough); mNavigationBarAlwaysShowOnSideGesture = res.getBoolean(R.bool.config_navBarAlwaysShowOnSideEdgeGesture); - // This should calculate how much above the frame we accept gestures. - mBottomGestureAdditionalInset = - res.getDimensionPixelSize(R.dimen.navigation_bar_gesture_height) - - getNavigationBarFrameHeight(portraitRotation); - updateConfigurationAndScreenSizeDependentBehaviors(); final boolean shouldAttach = @@ -2221,17 +2123,8 @@ public class DisplayPolicy { return; } - final @InsetsType int restorePositionTypes = - (controlTarget.getRequestedVisibility(ITYPE_NAVIGATION_BAR) - ? Type.navigationBars() : 0) - | (controlTarget.getRequestedVisibility(ITYPE_STATUS_BAR) - ? Type.statusBars() : 0) - | (mExtraNavBarAlt != null && controlTarget.getRequestedVisibility( - ITYPE_EXTRA_NAVIGATION_BAR) - ? Type.navigationBars() : 0) - | (mClimateBarAlt != null && controlTarget.getRequestedVisibility( - ITYPE_CLIMATE_BAR) - ? Type.statusBars() : 0); + final @InsetsType int restorePositionTypes = (Type.statusBars() | Type.navigationBars()) + & controlTarget.getRequestedVisibleTypes(); if (swipeTarget == mNavigationBar && (restorePositionTypes & Type.navigationBars()) != 0) { @@ -2325,8 +2218,8 @@ public class DisplayPolicy { navColorWin) | opaqueAppearance; final int behavior = win.mAttrs.insetsFlags.behavior; final String focusedApp = win.mAttrs.packageName; - final boolean isFullscreen = !win.getRequestedVisibility(ITYPE_STATUS_BAR) - || !win.getRequestedVisibility(ITYPE_NAVIGATION_BAR); + final boolean isFullscreen = !win.isRequestedVisible(Type.statusBars()) + || !win.isRequestedVisible(Type.navigationBars()); final AppearanceRegion[] statusBarAppearanceRegions = new AppearanceRegion[mStatusBarAppearanceRegionList.size()]; mStatusBarAppearanceRegionList.toArray(statusBarAppearanceRegions); @@ -2336,11 +2229,12 @@ public class DisplayPolicy { callStatusBarSafely(statusBar -> statusBar.setDisableFlags(displayId, disableFlags, cause)); } + final @InsetsType int requestedVisibleTypes = win.getRequestedVisibleTypes(); final LetterboxDetails[] letterboxDetails = new LetterboxDetails[mLetterboxDetails.size()]; mLetterboxDetails.toArray(letterboxDetails); if (mLastAppearance == appearance && mLastBehavior == behavior - && mRequestedVisibilities.equals(win.getRequestedVisibilities()) + && mLastRequestedVisibleTypes == requestedVisibleTypes && Objects.equals(mFocusedApp, focusedApp) && mLastFocusIsFullscreen == isFullscreen && Arrays.equals(mLastStatusBarAppearanceRegions, statusBarAppearanceRegions) @@ -2353,9 +2247,12 @@ public class DisplayPolicy { isFullscreen || (appearance & APPEARANCE_LOW_PROFILE_BARS) != 0); } final InsetsVisibilities requestedVisibilities = - new InsetsVisibilities(win.getRequestedVisibilities()); + mLastRequestedVisibleTypes == requestedVisibleTypes + ? mRequestedVisibilities + : toInsetsVisibilities(requestedVisibleTypes); mLastAppearance = appearance; mLastBehavior = behavior; + mLastRequestedVisibleTypes = requestedVisibleTypes; mRequestedVisibilities = requestedVisibilities; mFocusedApp = focusedApp; mLastFocusIsFullscreen = isFullscreen; @@ -2366,6 +2263,20 @@ public class DisplayPolicy { requestedVisibilities, focusedApp, letterboxDetails)); } + // TODO (253420890): Remove this when removing mRequestedVisibilities. + private static InsetsVisibilities toInsetsVisibilities(@InsetsType int requestedVisibleTypes) { + final @InsetsType int defaultVisibleTypes = WindowInsets.Type.defaultVisible(); + final InsetsVisibilities insetsVisibilities = new InsetsVisibilities(); + for (@InternalInsetsType int i = InsetsState.SIZE - 1; i >= 0; i--) { + @InsetsType int type = InsetsState.toPublicType(i); + if ((type & (requestedVisibleTypes ^ defaultVisibleTypes)) != 0) { + // We only set the visibility if it is different from the default one. + insetsVisibilities.setVisibility(i, (type & requestedVisibleTypes) != 0); + } + } + return insetsVisibilities; + } + private void callStatusBarSafely(Consumer<StatusBarManagerInternal> consumer) { mHandler.post(() -> { StatusBarManagerInternal statusBar = getStatusBarManagerInternal(); @@ -2456,7 +2367,7 @@ public class DisplayPolicy { appearance = configureNavBarOpacity(appearance, multiWindowTaskVisible, freeformRootTaskVisible); - final boolean requestHideNavBar = !win.getRequestedVisibility(ITYPE_NAVIGATION_BAR); + final boolean requestHideNavBar = !win.isRequestedVisible(Type.navigationBars()); final long now = SystemClock.uptimeMillis(); final boolean pendingPanic = mPendingPanicGestureUptime != 0 && now - mPendingPanicGestureUptime <= PANIC_GESTURE_EXPIRATION; diff --git a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java index 14a1cd011ad6..38eca359c2ba 100644 --- a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java +++ b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java @@ -34,6 +34,7 @@ import android.os.Trace; import android.util.proto.ProtoOutputStream; import android.view.InsetsSource; import android.view.InsetsSourceControl; +import android.view.InsetsState; import android.view.WindowInsets; import android.window.TaskSnapshot; @@ -104,7 +105,7 @@ final class ImeInsetsSourceProvider extends WindowContainerInsetsSourceProvider @Override protected boolean updateClientVisibility(InsetsControlTarget caller) { boolean changed = super.updateClientVisibility(caller); - if (changed && caller.getRequestedVisibility(mSource.getType())) { + if (changed && caller.isRequestedVisible(InsetsState.toPublicType(mSource.getType()))) { reportImeDrawnForOrganizer(caller); } return changed; diff --git a/services/core/java/com/android/server/wm/InsetsControlTarget.java b/services/core/java/com/android/server/wm/InsetsControlTarget.java index 287dd74e62c7..d35b7c3d5fbe 100644 --- a/services/core/java/com/android/server/wm/InsetsControlTarget.java +++ b/services/core/java/com/android/server/wm/InsetsControlTarget.java @@ -17,8 +17,7 @@ package com.android.server.wm; import android.inputmethodservice.InputMethodService; -import android.view.InsetsState; -import android.view.InsetsState.InternalInsetsType; +import android.view.WindowInsets; import android.view.WindowInsets.Type.InsetsType; /** @@ -40,10 +39,17 @@ interface InsetsControlTarget { } /** - * @return The requested visibility of this target. + * @return {@code true} if any of the {@link InsetsType} is requested visible by this target. */ - default boolean getRequestedVisibility(@InternalInsetsType int type) { - return InsetsState.getDefaultVisibility(type); + default boolean isRequestedVisible(@InsetsType int types) { + return (WindowInsets.Type.defaultVisible() & types) != 0; + } + + /** + * @return {@link InsetsType}s which are requested visible by this target. + */ + default @InsetsType int getRequestedVisibleTypes() { + return WindowInsets.Type.defaultVisible(); } /** diff --git a/services/core/java/com/android/server/wm/InsetsPolicy.java b/services/core/java/com/android/server/wm/InsetsPolicy.java index 2de8faf8b086..b9fa80cf2c0f 100644 --- a/services/core/java/com/android/server/wm/InsetsPolicy.java +++ b/services/core/java/com/android/server/wm/InsetsPolicy.java @@ -177,8 +177,8 @@ class InsetsPolicy { : navControlTarget == notificationShade ? getNavControlTarget(topApp, true /* fake */) : null); - mStatusBar.updateVisibility(statusControlTarget, ITYPE_STATUS_BAR); - mNavBar.updateVisibility(navControlTarget, ITYPE_NAVIGATION_BAR); + mStatusBar.updateVisibility(statusControlTarget, Type.statusBars()); + mNavBar.updateVisibility(navControlTarget, Type.navigationBars()); } boolean isHidden(@InternalInsetsType int type) { @@ -455,7 +455,7 @@ class InsetsPolicy { if (originalImeSource != null) { final boolean imeVisibility = - w.mActivityRecord.mLastImeShown || w.getRequestedVisibility(ITYPE_IME); + w.mActivityRecord.mLastImeShown || w.isRequestedVisible(Type.ime()); final InsetsState state = copyState ? new InsetsState(originalState) : originalState; final InsetsSource imeSource = new InsetsSource(originalImeSource); @@ -501,11 +501,11 @@ class InsetsPolicy { private void checkAbortTransient(InsetsControlTarget caller) { if (mShowingTransientTypes.size() != 0) { final IntArray abortTypes = new IntArray(); - final boolean imeRequestedVisible = caller.getRequestedVisibility(ITYPE_IME); + final boolean imeRequestedVisible = caller.isRequestedVisible(Type.ime()); for (int i = mShowingTransientTypes.size() - 1; i >= 0; i--) { final @InternalInsetsType int type = mShowingTransientTypes.get(i); if ((mStateController.isFakeTarget(type, caller) - && caller.getRequestedVisibility(type)) + && caller.isRequestedVisible(InsetsState.toPublicType(type))) || (type == ITYPE_NAVIGATION_BAR && imeRequestedVisible)) { mShowingTransientTypes.remove(i); abortTypes.add(type); @@ -552,7 +552,7 @@ class InsetsPolicy { ComponentName component = focusedWin.mActivityRecord != null ? focusedWin.mActivityRecord.mActivityComponent : null; mDisplayContent.mRemoteInsetsControlTarget.topFocusedWindowChanged( - component, focusedWin.getRequestedVisibilities()); + component, focusedWin.getRequestedVisibleTypes()); return mDisplayContent.mRemoteInsetsControlTarget; } if (mPolicy.areSystemBarsForcedShownLw()) { @@ -612,7 +612,7 @@ class InsetsPolicy { ComponentName component = focusedWin.mActivityRecord != null ? focusedWin.mActivityRecord.mActivityComponent : null; mDisplayContent.mRemoteInsetsControlTarget.topFocusedWindowChanged( - component, focusedWin.getRequestedVisibilities()); + component, focusedWin.getRequestedVisibleTypes()); return mDisplayContent.mRemoteInsetsControlTarget; } if (mPolicy.areSystemBarsForcedShownLw()) { @@ -734,8 +734,8 @@ class InsetsPolicy { } private void updateVisibility(@Nullable InsetsControlTarget controlTarget, - @InternalInsetsType int type) { - setVisible(controlTarget == null || controlTarget.getRequestedVisibility(type)); + @Type.InsetsType int type) { + setVisible(controlTarget == null || controlTarget.isRequestedVisible(type)); } private void setVisible(boolean visible) { diff --git a/services/core/java/com/android/server/wm/InsetsSourceProvider.java b/services/core/java/com/android/server/wm/InsetsSourceProvider.java index 3a8fbbbaa77d..5b205f0bf59d 100644 --- a/services/core/java/com/android/server/wm/InsetsSourceProvider.java +++ b/services/core/java/com/android/server/wm/InsetsSourceProvider.java @@ -48,6 +48,7 @@ import android.view.InsetsSourceControl; import android.view.InsetsState; import android.view.SurfaceControl; import android.view.SurfaceControl.Transaction; +import android.view.WindowInsets; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; @@ -458,8 +459,9 @@ abstract class InsetsSourceProvider { } final Point surfacePosition = getWindowFrameSurfacePosition(); mAdapter = new ControlAdapter(surfacePosition); - if (getSource().getType() == ITYPE_IME) { - setClientVisible(target.getRequestedVisibility(mSource.getType())); + final int type = getSource().getType(); + if (type == ITYPE_IME) { + setClientVisible(target.isRequestedVisible(WindowInsets.Type.ime())); } final Transaction t = mDisplayContent.getSyncTransaction(); mWindowContainer.startAnimation(t, mAdapter, !mClientVisible /* hidden */, @@ -472,8 +474,8 @@ abstract class InsetsSourceProvider { final SurfaceControl leash = mAdapter.mCapturedLeash; mControlTarget = target; updateVisibility(); - mControl = new InsetsSourceControl(mSource.getType(), leash, mClientVisible, - surfacePosition, mInsetsHint); + mControl = new InsetsSourceControl(type, leash, mClientVisible, surfacePosition, + mInsetsHint); ProtoLog.d(WM_DEBUG_WINDOW_INSETS, "InsetsSource Control %s for target %s", mControl, mControlTarget); @@ -491,7 +493,8 @@ abstract class InsetsSourceProvider { } boolean updateClientVisibility(InsetsControlTarget caller) { - final boolean requestedVisible = caller.getRequestedVisibility(mSource.getType()); + final boolean requestedVisible = + caller.isRequestedVisible(InsetsState.toPublicType(mSource.getType())); if (caller != mControlTarget || requestedVisible == mClientVisible) { return false; } diff --git a/services/core/java/com/android/server/wm/LetterboxConfiguration.java b/services/core/java/com/android/server/wm/LetterboxConfiguration.java index a469c6b39e7f..c19353cb2676 100644 --- a/services/core/java/com/android/server/wm/LetterboxConfiguration.java +++ b/services/core/java/com/android/server/wm/LetterboxConfiguration.java @@ -17,14 +17,17 @@ package com.android.server.wm; import android.annotation.IntDef; +import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; import android.graphics.Color; import com.android.internal.R; +import com.android.internal.annotations.VisibleForTesting; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.function.Function; /** Reads letterbox configs from resources and controls their overrides at runtime. */ final class LetterboxConfiguration { @@ -156,34 +159,25 @@ final class LetterboxConfiguration { // portrait device orientation. private boolean mIsVerticalReachabilityEnabled; - - // Horizontal position of a center of the letterboxed app window which is global to prevent - // "jumps" when switching between letterboxed apps. It's updated to reposition the app window - // in response to a double tap gesture (see LetterboxUiController#handleDoubleTap). Used in - // LetterboxUiController#getHorizontalPositionMultiplier which is called from - // ActivityRecord#updateResolvedBoundsPosition. - // TODO(b/199426138): Global reachability setting causes a jump when resuming an app from - // Overview after changing position in another app. - @LetterboxHorizontalReachabilityPosition - private volatile int mLetterboxPositionForHorizontalReachability; - - // Vertical position of a center of the letterboxed app window which is global to prevent - // "jumps" when switching between letterboxed apps. It's updated to reposition the app window - // in response to a double tap gesture (see LetterboxUiController#handleDoubleTap). Used in - // LetterboxUiController#getVerticalPositionMultiplier which is called from - // ActivityRecord#updateResolvedBoundsPosition. - // TODO(b/199426138): Global reachability setting causes a jump when resuming an app from - // Overview after changing position in another app. - @LetterboxVerticalReachabilityPosition - private volatile int mLetterboxPositionForVerticalReachability; - // Whether education is allowed for letterboxed fullscreen apps. private boolean mIsEducationEnabled; // Whether using split screen aspect ratio as a default aspect ratio for unresizable apps. private boolean mIsSplitScreenAspectRatioForUnresizableAppsEnabled; + // Responsible for the persistence of letterbox[Horizontal|Vertical]PositionMultiplier + @NonNull + private final LetterboxConfigurationPersister mLetterboxConfigurationPersister; + LetterboxConfiguration(Context systemUiContext) { + this(systemUiContext, new LetterboxConfigurationPersister(systemUiContext, + () -> readLetterboxHorizontalReachabilityPositionFromConfig(systemUiContext), + () -> readLetterboxVerticalReachabilityPositionFromConfig(systemUiContext))); + } + + @VisibleForTesting + LetterboxConfiguration(Context systemUiContext, + LetterboxConfigurationPersister letterboxConfigurationPersister) { mContext = systemUiContext; mFixedOrientationLetterboxAspectRatio = mContext.getResources().getFloat( R.dimen.config_fixedOrientationLetterboxAspectRatio); @@ -206,14 +200,14 @@ final class LetterboxConfiguration { readLetterboxHorizontalReachabilityPositionFromConfig(mContext); mDefaultPositionForVerticalReachability = readLetterboxVerticalReachabilityPositionFromConfig(mContext); - mLetterboxPositionForHorizontalReachability = mDefaultPositionForHorizontalReachability; - mLetterboxPositionForVerticalReachability = mDefaultPositionForVerticalReachability; mIsEducationEnabled = mContext.getResources().getBoolean( R.bool.config_letterboxIsEducationEnabled); setDefaultMinAspectRatioForUnresizableApps(mContext.getResources().getFloat( R.dimen.config_letterboxDefaultMinAspectRatioForUnresizableApps)); mIsSplitScreenAspectRatioForUnresizableAppsEnabled = mContext.getResources().getBoolean( R.bool.config_letterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled); + mLetterboxConfigurationPersister = letterboxConfigurationPersister; + mLetterboxConfigurationPersister.start(); } /** @@ -653,7 +647,9 @@ final class LetterboxConfiguration { * <p>The position multiplier is changed after each double tap in the letterbox area. */ float getHorizontalMultiplierForReachability() { - switch (mLetterboxPositionForHorizontalReachability) { + final int letterboxPositionForHorizontalReachability = + mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability(); + switch (letterboxPositionForHorizontalReachability) { case LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT: return 0.0f; case LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER: @@ -662,10 +658,11 @@ final class LetterboxConfiguration { return 1.0f; default: throw new AssertionError( - "Unexpected letterbox position type: " - + mLetterboxPositionForHorizontalReachability); + "Unexpected letterbox position type: " + + letterboxPositionForHorizontalReachability); } } + /* * Gets vertical position of a center of the letterboxed app window when reachability * is enabled specified. 0 corresponds to the top side of the screen and 1 to the bottom side. @@ -673,7 +670,9 @@ final class LetterboxConfiguration { * <p>The position multiplier is changed after each double tap in the letterbox area. */ float getVerticalMultiplierForReachability() { - switch (mLetterboxPositionForVerticalReachability) { + final int letterboxPositionForVerticalReachability = + mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability(); + switch (letterboxPositionForVerticalReachability) { case LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP: return 0.0f; case LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER: @@ -683,7 +682,7 @@ final class LetterboxConfiguration { default: throw new AssertionError( "Unexpected letterbox position type: " - + mLetterboxPositionForVerticalReachability); + + letterboxPositionForVerticalReachability); } } @@ -693,7 +692,7 @@ final class LetterboxConfiguration { */ @LetterboxHorizontalReachabilityPosition int getLetterboxPositionForHorizontalReachability() { - return mLetterboxPositionForHorizontalReachability; + return mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability(); } /* @@ -702,7 +701,7 @@ final class LetterboxConfiguration { */ @LetterboxVerticalReachabilityPosition int getLetterboxPositionForVerticalReachability() { - return mLetterboxPositionForVerticalReachability; + return mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability(); } /** Returns a string representing the given {@link LetterboxHorizontalReachabilityPosition}. */ @@ -742,9 +741,8 @@ final class LetterboxConfiguration { * right side. */ void movePositionForHorizontalReachabilityToNextRightStop() { - mLetterboxPositionForHorizontalReachability = Math.min( - mLetterboxPositionForHorizontalReachability + 1, - LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT); + updatePositionForHorizontalReachability(prev -> Math.min( + prev + 1, LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT)); } /** @@ -752,8 +750,7 @@ final class LetterboxConfiguration { * side. */ void movePositionForHorizontalReachabilityToNextLeftStop() { - mLetterboxPositionForHorizontalReachability = - Math.max(mLetterboxPositionForHorizontalReachability - 1, 0); + updatePositionForHorizontalReachability(prev -> Math.max(prev - 1, 0)); } /** @@ -761,9 +758,8 @@ final class LetterboxConfiguration { * side. */ void movePositionForVerticalReachabilityToNextBottomStop() { - mLetterboxPositionForVerticalReachability = Math.min( - mLetterboxPositionForVerticalReachability + 1, - LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM); + updatePositionForVerticalReachability(prev -> Math.min( + prev + 1, LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM)); } /** @@ -771,8 +767,7 @@ final class LetterboxConfiguration { * side. */ void movePositionForVerticalReachabilityToNextTopStop() { - mLetterboxPositionForVerticalReachability = - Math.max(mLetterboxPositionForVerticalReachability - 1, 0); + updatePositionForVerticalReachability(prev -> Math.max(prev - 1, 0)); } /** @@ -822,4 +817,26 @@ final class LetterboxConfiguration { R.bool.config_letterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled); } + /** Calculates a new letterboxPositionForHorizontalReachability value and updates the store */ + private void updatePositionForHorizontalReachability( + Function<Integer, Integer> newHorizonalPositionFun) { + final int letterboxPositionForHorizontalReachability = + mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability(); + final int nextHorizontalPosition = newHorizonalPositionFun.apply( + letterboxPositionForHorizontalReachability); + mLetterboxConfigurationPersister.setLetterboxPositionForHorizontalReachability( + nextHorizontalPosition); + } + + /** Calculates a new letterboxPositionForVerticalReachability value and updates the store */ + private void updatePositionForVerticalReachability( + Function<Integer, Integer> newVerticalPositionFun) { + final int letterboxPositionForVerticalReachability = + mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability(); + final int nextVerticalPosition = newVerticalPositionFun.apply( + letterboxPositionForVerticalReachability); + mLetterboxConfigurationPersister.setLetterboxPositionForVerticalReachability( + nextVerticalPosition); + } + } diff --git a/services/core/java/com/android/server/wm/LetterboxConfigurationPersister.java b/services/core/java/com/android/server/wm/LetterboxConfigurationPersister.java new file mode 100644 index 000000000000..70639b16c828 --- /dev/null +++ b/services/core/java/com/android/server/wm/LetterboxConfigurationPersister.java @@ -0,0 +1,259 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm; + +import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME; +import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.os.Environment; +import android.util.AtomicFile; +import android.util.Slog; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.wm.LetterboxConfiguration.LetterboxHorizontalReachabilityPosition; +import com.android.server.wm.LetterboxConfiguration.LetterboxVerticalReachabilityPosition; +import com.android.server.wm.nano.WindowManagerProtos; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * Persists the values of letterboxPositionForHorizontalReachability and + * letterboxPositionForVerticalReachability for {@link LetterboxConfiguration}. + */ +class LetterboxConfigurationPersister { + + private static final String TAG = + TAG_WITH_CLASS_NAME ? "LetterboxConfigurationPersister" : TAG_WM; + + @VisibleForTesting + static final String LETTERBOX_CONFIGURATION_FILENAME = "letterbox_config"; + + private final Context mContext; + private final Supplier<Integer> mDefaultHorizontalReachabilitySupplier; + private final Supplier<Integer> mDefaultVerticalReachabilitySupplier; + + // Horizontal position of a center of the letterboxed app window which is global to prevent + // "jumps" when switching between letterboxed apps. It's updated to reposition the app window + // in response to a double tap gesture (see LetterboxUiController#handleDoubleTap). Used in + // LetterboxUiController#getHorizontalPositionMultiplier which is called from + // ActivityRecord#updateResolvedBoundsPosition. + @LetterboxHorizontalReachabilityPosition + private volatile int mLetterboxPositionForHorizontalReachability; + + // Vertical position of a center of the letterboxed app window which is global to prevent + // "jumps" when switching between letterboxed apps. It's updated to reposition the app window + // in response to a double tap gesture (see LetterboxUiController#handleDoubleTap). Used in + // LetterboxUiController#getVerticalPositionMultiplier which is called from + // ActivityRecord#updateResolvedBoundsPosition. + @LetterboxVerticalReachabilityPosition + private volatile int mLetterboxPositionForVerticalReachability; + + @NonNull + private final AtomicFile mConfigurationFile; + + @Nullable + private final Consumer<String> mCompletionCallback; + + @NonNull + private final PersisterQueue mPersisterQueue; + + LetterboxConfigurationPersister(Context systemUiContext, + Supplier<Integer> defaultHorizontalReachabilitySupplier, + Supplier<Integer> defaultVerticalReachabilitySupplier) { + this(systemUiContext, defaultHorizontalReachabilitySupplier, + defaultVerticalReachabilitySupplier, + Environment.getDataSystemDirectory(), new PersisterQueue(), + /* completionCallback */ null); + } + + @VisibleForTesting + LetterboxConfigurationPersister(Context systemUiContext, + Supplier<Integer> defaultHorizontalReachabilitySupplier, + Supplier<Integer> defaultVerticalReachabilitySupplier, File configFolder, + PersisterQueue persisterQueue, @Nullable Consumer<String> completionCallback) { + mContext = systemUiContext.createDeviceProtectedStorageContext(); + mDefaultHorizontalReachabilitySupplier = defaultHorizontalReachabilitySupplier; + mDefaultVerticalReachabilitySupplier = defaultVerticalReachabilitySupplier; + mCompletionCallback = completionCallback; + final File prefFiles = new File(configFolder, LETTERBOX_CONFIGURATION_FILENAME); + mConfigurationFile = new AtomicFile(prefFiles); + mPersisterQueue = persisterQueue; + readCurrentConfiguration(); + } + + /** + * Startes the persistence queue + */ + void start() { + mPersisterQueue.startPersisting(); + } + + /* + * Gets the horizontal position of the letterboxed app window when horizontal reachability is + * enabled. + */ + @LetterboxHorizontalReachabilityPosition + int getLetterboxPositionForHorizontalReachability() { + return mLetterboxPositionForHorizontalReachability; + } + + /* + * Gets the vertical position of the letterboxed app window when vertical reachability is + * enabled. + */ + @LetterboxVerticalReachabilityPosition + int getLetterboxPositionForVerticalReachability() { + return mLetterboxPositionForVerticalReachability; + } + + /** + * Updates letterboxPositionForVerticalReachability if different from the current value + */ + void setLetterboxPositionForHorizontalReachability( + int letterboxPositionForHorizontalReachability) { + if (mLetterboxPositionForHorizontalReachability + != letterboxPositionForHorizontalReachability) { + mLetterboxPositionForHorizontalReachability = + letterboxPositionForHorizontalReachability; + updateConfiguration(); + } + } + + /** + * Updates letterboxPositionForVerticalReachability if different from the current value + */ + void setLetterboxPositionForVerticalReachability( + int letterboxPositionForVerticalReachability) { + if (mLetterboxPositionForVerticalReachability != letterboxPositionForVerticalReachability) { + mLetterboxPositionForVerticalReachability = letterboxPositionForVerticalReachability; + updateConfiguration(); + } + } + + @VisibleForTesting + void useDefaultValue() { + mLetterboxPositionForHorizontalReachability = mDefaultHorizontalReachabilitySupplier.get(); + mLetterboxPositionForVerticalReachability = mDefaultVerticalReachabilitySupplier.get(); + } + + private void readCurrentConfiguration() { + FileInputStream fis = null; + try { + fis = mConfigurationFile.openRead(); + byte[] protoData = readInputStream(fis); + final WindowManagerProtos.LetterboxProto letterboxData = + WindowManagerProtos.LetterboxProto.parseFrom(protoData); + mLetterboxPositionForHorizontalReachability = + letterboxData.letterboxPositionForHorizontalReachability; + mLetterboxPositionForVerticalReachability = + letterboxData.letterboxPositionForVerticalReachability; + } catch (IOException ioe) { + Slog.e(TAG, + "Error reading from LetterboxConfigurationPersister. " + + "Using default values!", ioe); + useDefaultValue(); + } finally { + if (fis != null) { + try { + fis.close(); + } catch (IOException e) { + useDefaultValue(); + Slog.e(TAG, "Error reading from LetterboxConfigurationPersister ", e); + } + } + } + } + + private void updateConfiguration() { + mPersisterQueue.addItem(new UpdateValuesCommand(mConfigurationFile, + mLetterboxPositionForHorizontalReachability, + mLetterboxPositionForVerticalReachability, + mCompletionCallback), /* flush */ true); + } + + private static byte[] readInputStream(InputStream in) throws IOException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + try { + byte[] buffer = new byte[1024]; + int size = in.read(buffer); + while (size > 0) { + outputStream.write(buffer, 0, size); + size = in.read(buffer); + } + return outputStream.toByteArray(); + } finally { + outputStream.close(); + } + } + + private static class UpdateValuesCommand implements + PersisterQueue.WriteQueueItem<UpdateValuesCommand> { + + @NonNull + private final AtomicFile mFileToUpdate; + @Nullable + private final Consumer<String> mOnComplete; + + + private final int mHorizontalReachability; + private final int mVerticalReachability; + + UpdateValuesCommand(@NonNull AtomicFile fileToUpdate, + int horizontalReachability, int verticalReachability, + @Nullable Consumer<String> onComplete) { + mFileToUpdate = fileToUpdate; + mHorizontalReachability = horizontalReachability; + mVerticalReachability = verticalReachability; + mOnComplete = onComplete; + } + + @Override + public void process() { + final WindowManagerProtos.LetterboxProto letterboxData = + new WindowManagerProtos.LetterboxProto(); + letterboxData.letterboxPositionForHorizontalReachability = mHorizontalReachability; + letterboxData.letterboxPositionForVerticalReachability = mVerticalReachability; + final byte[] bytes = WindowManagerProtos.LetterboxProto.toByteArray(letterboxData); + + FileOutputStream fos = null; + try { + fos = mFileToUpdate.startWrite(); + fos.write(bytes); + mFileToUpdate.finishWrite(fos); + } catch (IOException ioe) { + mFileToUpdate.failWrite(fos); + Slog.e(TAG, + "Error writing to LetterboxConfigurationPersister. " + + "Using default values!", ioe); + } finally { + if (mOnComplete != null) { + mOnComplete.accept("UpdateValuesCommand"); + } + } + } + } +} diff --git a/services/core/java/com/android/server/wm/RemoteAnimationController.java b/services/core/java/com/android/server/wm/RemoteAnimationController.java index 8db5289f8b45..d34e610fa0fd 100644 --- a/services/core/java/com/android/server/wm/RemoteAnimationController.java +++ b/services/core/java/com/android/server/wm/RemoteAnimationController.java @@ -323,11 +323,11 @@ class RemoteAnimationController implements DeathRecipient { mService.closeSurfaceTransaction("RemoteAnimationController#finished"); mIsFinishing = false; } + // Reset input for all activities when the remote animation is finished. + final Consumer<ActivityRecord> updateActivities = + activity -> activity.setDropInputForAnimation(false); + mDisplayContent.forAllActivities(updateActivities); } - // Reset input for all activities when the remote animation is finished. - final Consumer<ActivityRecord> updateActivities = - activity -> activity.setDropInputForAnimation(false); - mDisplayContent.forAllActivities(updateActivities); setRunningRemoteAnimation(false); ProtoLog.i(WM_DEBUG_REMOTE_ANIMATIONS, "Finishing remote animation"); } diff --git a/services/core/java/com/android/server/wm/Session.java b/services/core/java/com/android/server/wm/Session.java index 9660fe2142ae..b4821816977f 100644 --- a/services/core/java/com/android/server/wm/Session.java +++ b/services/core/java/com/android/server/wm/Session.java @@ -69,10 +69,11 @@ import android.view.IWindowSessionCallback; import android.view.InputChannel; import android.view.InsetsSourceControl; import android.view.InsetsState; -import android.view.InsetsVisibilities; import android.view.SurfaceControl; import android.view.SurfaceSession; import android.view.View; +import android.view.WindowInsets; +import android.view.WindowInsets.Type.InsetsType; import android.view.WindowManager; import android.window.ClientWindowFrames; import android.window.OnBackInvokedCallbackInfo; @@ -117,7 +118,6 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient { private float mLastReportedAnimatorScale; private String mPackageName; private String mRelayoutTag; - private final InsetsVisibilities mDummyRequestedVisibilities = new InsetsVisibilities(); private final InsetsSourceControl[] mDummyControls = new InsetsSourceControl[0]; final boolean mSetsUnrestrictedKeepClearAreas; @@ -196,23 +196,23 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient { @Override public int addToDisplay(IWindow window, WindowManager.LayoutParams attrs, - int viewVisibility, int displayId, InsetsVisibilities requestedVisibilities, + int viewVisibility, int displayId, @InsetsType int requestedVisibleTypes, InputChannel outInputChannel, InsetsState outInsetsState, InsetsSourceControl[] outActiveControls, Rect outAttachedFrame, float[] outSizeCompatScale) { return mService.addWindow(this, window, attrs, viewVisibility, displayId, - UserHandle.getUserId(mUid), requestedVisibilities, outInputChannel, outInsetsState, + UserHandle.getUserId(mUid), requestedVisibleTypes, outInputChannel, outInsetsState, outActiveControls, outAttachedFrame, outSizeCompatScale); } @Override public int addToDisplayAsUser(IWindow window, WindowManager.LayoutParams attrs, - int viewVisibility, int displayId, int userId, InsetsVisibilities requestedVisibilities, + int viewVisibility, int displayId, int userId, @InsetsType int requestedVisibleTypes, InputChannel outInputChannel, InsetsState outInsetsState, InsetsSourceControl[] outActiveControls, Rect outAttachedFrame, float[] outSizeCompatScale) { return mService.addWindow(this, window, attrs, viewVisibility, displayId, userId, - requestedVisibilities, outInputChannel, outInsetsState, outActiveControls, + requestedVisibleTypes, outInputChannel, outInsetsState, outActiveControls, outAttachedFrame, outSizeCompatScale); } @@ -221,8 +221,9 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient { int viewVisibility, int displayId, InsetsState outInsetsState, Rect outAttachedFrame, float[] outSizeCompatScale) { return mService.addWindow(this, window, attrs, viewVisibility, displayId, - UserHandle.getUserId(mUid), mDummyRequestedVisibilities, null /* outInputChannel */, - outInsetsState, mDummyControls, outAttachedFrame, outSizeCompatScale); + UserHandle.getUserId(mUid), WindowInsets.Type.defaultVisible(), + null /* outInputChannel */, outInsetsState, mDummyControls, outAttachedFrame, + outSizeCompatScale); } @Override @@ -683,12 +684,12 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient { } @Override - public void updateRequestedVisibilities(IWindow window, InsetsVisibilities visibilities) { + public void updateRequestedVisibleTypes(IWindow window, @InsetsType int requestedVisibleTypes) { synchronized (mService.mGlobalLock) { final WindowState windowState = mService.windowForClientLocked(this, window, false /* throwOnError */); if (windowState != null) { - windowState.setRequestedVisibilities(visibilities); + windowState.setRequestedVisibleTypes(requestedVisibleTypes); windowState.getDisplayContent().getInsetsPolicy().onInsetsModified(windowState); } } diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index e29e3a2abc13..435ab9723c1c 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -3513,7 +3513,7 @@ class Task extends TaskFragment { final WindowState topMainWin = getWindow(w -> w.mAttrs.type == TYPE_BASE_APPLICATION); if (topMainWin != null) { info.mainWindowLayoutParams = topMainWin.getAttrs(); - info.requestedVisibilities.set(topMainWin.getRequestedVisibilities()); + info.requestedVisibleTypes = topMainWin.getRequestedVisibleTypes(); } } // If the developer has persist a different configuration, we need to override it to the diff --git a/services/core/java/com/android/server/wm/TaskSnapshotController.java b/services/core/java/com/android/server/wm/TaskSnapshotController.java index 9306749f17b9..29c98b9f8b01 100644 --- a/services/core/java/com/android/server/wm/TaskSnapshotController.java +++ b/services/core/java/com/android/server/wm/TaskSnapshotController.java @@ -587,7 +587,7 @@ class TaskSnapshotController { final Rect systemBarInsets = getSystemBarInsets(mainWindow.getFrame(), insetsState); final SystemBarBackgroundPainter decorPainter = new SystemBarBackgroundPainter(attrs.flags, attrs.privateFlags, attrs.insetsFlags.appearance, task.getTaskDescription(), - mHighResTaskSnapshotScale, insetsState); + mHighResTaskSnapshotScale, mainWindow.getRequestedVisibleTypes()); final int taskWidth = taskBounds.width(); final int taskHeight = taskBounds.height(); final int width = (int) (taskWidth * mHighResTaskSnapshotScale); @@ -750,12 +750,12 @@ class TaskSnapshotController { private final int mWindowFlags; private final int mWindowPrivateFlags; private final float mScale; - private final InsetsState mInsetsState; + private final @Type.InsetsType int mRequestedVisibleTypes; private final Rect mSystemBarInsets = new Rect(); SystemBarBackgroundPainter(int windowFlags, int windowPrivateFlags, int appearance, ActivityManager.TaskDescription taskDescription, float scale, - InsetsState insetsState) { + @Type.InsetsType int requestedVisibleTypes) { mWindowFlags = windowFlags; mWindowPrivateFlags = windowPrivateFlags; mScale = scale; @@ -774,7 +774,7 @@ class TaskSnapshotController { && context.getResources().getBoolean(R.bool.config_navBarNeedsScrim)); mStatusBarPaint.setColor(mStatusBarColor); mNavigationBarPaint.setColor(mNavigationBarColor); - mInsetsState = insetsState; + mRequestedVisibleTypes = requestedVisibleTypes; } void setInsets(Rect systemBarInsets) { @@ -785,7 +785,7 @@ class TaskSnapshotController { final boolean forceBarBackground = (mWindowPrivateFlags & PRIVATE_FLAG_FORCE_DRAW_BAR_BACKGROUNDS) != 0; if (STATUS_BAR_COLOR_VIEW_ATTRIBUTES.isVisible( - mInsetsState, mStatusBarColor, mWindowFlags, forceBarBackground)) { + mRequestedVisibleTypes, mStatusBarColor, mWindowFlags, forceBarBackground)) { return (int) (mSystemBarInsets.top * mScale); } else { return 0; @@ -796,7 +796,7 @@ class TaskSnapshotController { final boolean forceBarBackground = (mWindowPrivateFlags & PRIVATE_FLAG_FORCE_DRAW_BAR_BACKGROUNDS) != 0; return NAVIGATION_BAR_COLOR_VIEW_ATTRIBUTES.isVisible( - mInsetsState, mNavigationBarColor, mWindowFlags, forceBarBackground); + mRequestedVisibleTypes, mNavigationBarColor, mWindowFlags, forceBarBackground); } void drawDecors(Canvas c) { diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java index c4c66d8c9c3c..0b5de85c5cab 100644 --- a/services/core/java/com/android/server/wm/WindowContainer.java +++ b/services/core/java/com/android/server/wm/WindowContainer.java @@ -41,6 +41,7 @@ import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_SYNC_ENGINE; import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER; import static com.android.server.wm.AppTransition.MAX_APP_TRANSITION_DURATION; import static com.android.server.wm.AppTransition.isActivityTransitOld; +import static com.android.server.wm.AppTransition.isTaskFragmentTransitOld; import static com.android.server.wm.AppTransition.isTaskTransitOld; import static com.android.server.wm.DisplayContent.IME_TARGET_LAYERING; import static com.android.server.wm.IdentifierProto.HASH_CODE; @@ -2999,10 +3000,17 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< // {@link Activity#overridePendingTransition(int, int, int)}. @ColorInt int backdropColor = 0; if (controller.isFromActivityEmbedding()) { - final int animAttr = AppTransition.mapOpenCloseTransitTypes(transit, enter); - final Animation a = animAttr != 0 - ? appTransition.loadAnimationAttr(lp, animAttr, transit) : null; - showBackdrop = a != null && a.getShowBackdrop(); + if (isChanging) { + // When there are more than one changing containers, it may leave part of the + // screen empty. Show background color to cover that. + showBackdrop = getDisplayContent().mChangingContainers.size() > 1; + } else { + // Check whether or not to show backdrop for open/close transition. + final int animAttr = AppTransition.mapOpenCloseTransitTypes(transit, enter); + final Animation a = animAttr != 0 + ? appTransition.loadAnimationAttr(lp, animAttr, transit) : null; + showBackdrop = a != null && a.getShowBackdrop(); + } backdropColor = appTransition.getNextAppTransitionBackgroundColor(); } final Rect localBounds = new Rect(mTmpRect); @@ -3105,9 +3113,16 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< } } + // Check if the animation requests to show background color for Activity and embedded + // TaskFragment. final ActivityRecord activityRecord = asActivityRecord(); - if (activityRecord != null && isActivityTransitOld(transit) - && adapter.getShowBackground()) { + final TaskFragment taskFragment = asTaskFragment(); + if (adapter.getShowBackground() + // Check if it is Activity transition. + && ((activityRecord != null && isActivityTransitOld(transit)) + // Check if it is embedded TaskFragment transition. + || (taskFragment != null && taskFragment.isEmbedded() + && isTaskFragmentTransitOld(transit)))) { final @ColorInt int backgroundColorForTransition; if (adapter.getBackgroundColor() != 0) { // If available use the background color provided through getBackgroundColor @@ -3117,9 +3132,11 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< // Otherwise default to the window's background color if provided through // the theme as the background color for the animation - the top most window // with a valid background color and showBackground set takes precedence. - final Task arTask = activityRecord.getTask(); + final Task parentTask = activityRecord != null + ? activityRecord.getTask() + : taskFragment.getTask(); backgroundColorForTransition = ColorUtils.setAlphaComponent( - arTask.getTaskDescription().getBackgroundColor(), 255); + parentTask.getTaskDescription().getBackgroundColor(), 255); } animationRunnerBuilder.setTaskBackgroundColor(backgroundColorForTransition); } diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index c9d3dac104de..848c231bb568 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -266,9 +266,9 @@ import android.view.InputApplicationHandle; import android.view.InputChannel; import android.view.InputDevice; import android.view.InputWindowHandle; +import android.view.InsetsFrameProvider; import android.view.InsetsSourceControl; import android.view.InsetsState; -import android.view.InsetsVisibilities; import android.view.KeyEvent; import android.view.MagnificationSpec; import android.view.MotionEvent; @@ -283,6 +283,7 @@ import android.view.TaskTransitionSpec; import android.view.View; import android.view.WindowContentFrameStats; import android.view.WindowInsets; +import android.view.WindowInsets.Type.InsetsType; import android.view.WindowManager; import android.view.WindowManager.DisplayImePolicy; import android.view.WindowManager.LayoutParams; @@ -1409,7 +1410,7 @@ public class WindowManagerService extends IWindowManager.Stub } public int addWindow(Session session, IWindow client, LayoutParams attrs, int viewVisibility, - int displayId, int requestUserId, InsetsVisibilities requestedVisibilities, + int displayId, int requestUserId, @InsetsType int requestedVisibleTypes, InputChannel outInputChannel, InsetsState outInsetsState, InsetsSourceControl[] outActiveControls, Rect outAttachedFrame, float[] outSizeCompatScale) { @@ -1635,7 +1636,7 @@ public class WindowManagerService extends IWindowManager.Stub attrs.flags = sanitizeFlagSlippery(attrs.flags, win.getName(), callingUid, callingPid); attrs.inputFeatures = sanitizeSpyWindow(attrs.inputFeatures, win.getName(), callingUid, callingPid); - win.setRequestedVisibilities(requestedVisibilities); + win.setRequestedVisibleTypes(requestedVisibleTypes); res = displayPolicy.validateAddingWindowLw(attrs, callingPid, callingUid); if (res != ADD_OKAY) { @@ -2277,6 +2278,27 @@ public class WindowManagerService extends IWindowManager.Stub "Insets types can not be changed after the window is " + "added."); } + final InsetsFrameProvider.InsetsSizeOverride[] overrides = + win.mAttrs.providedInsets[i].insetsSizeOverrides; + final InsetsFrameProvider.InsetsSizeOverride[] newOverrides = + attrs.providedInsets[i].insetsSizeOverrides; + if (!(overrides == null && newOverrides == null)) { + if (overrides == null || newOverrides == null + || (overrides.length != newOverrides.length)) { + throw new IllegalArgumentException( + "Insets override types can not be changed after the " + + "window is added."); + } else { + final int overrideTypes = overrides.length; + for (int j = 0; j < overrideTypes; j++) { + if (overrides[j].windowType != newOverrides[j].windowType) { + throw new IllegalArgumentException( + "Insets override types can not be changed after" + + " the window is added."); + } + } + } + } } } } @@ -4428,7 +4450,8 @@ public class WindowManagerService extends IWindowManager.Stub } @Override - public void updateDisplayWindowRequestedVisibilities(int displayId, InsetsVisibilities vis) { + public void updateDisplayWindowRequestedVisibleTypes( + int displayId, @InsetsType int requestedVisibleTypes) { if (mContext.checkCallingOrSelfPermission(MANAGE_APP_TOKENS) != PackageManager.PERMISSION_GRANTED) { throw new SecurityException("Must hold permission " + MANAGE_APP_TOKENS); @@ -4440,7 +4463,7 @@ public class WindowManagerService extends IWindowManager.Stub if (dc == null || dc.mRemoteInsetsControlTarget == null) { return; } - dc.mRemoteInsetsControlTarget.setRequestedVisibilities(vis); + dc.mRemoteInsetsControlTarget.setRequestedVisibleTypes(requestedVisibleTypes); dc.getInsetsStateController().onInsetsModified(dc.mRemoteInsetsControlTarget); } } finally { diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 36389eacf27c..6d750a469f8c 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -30,7 +30,6 @@ import static android.os.PowerManager.DRAW_WAKE_LOCK; import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; import static android.view.InsetsState.ITYPE_IME; import static android.view.InsetsState.ITYPE_INVALID; -import static android.view.InsetsState.ITYPE_NAVIGATION_BAR; import static android.view.SurfaceControl.Transaction; import static android.view.SurfaceControl.getGlobalTransaction; import static android.view.ViewRootImpl.LOCAL_LAYOUT; @@ -41,6 +40,7 @@ import static android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_ import static android.view.WindowCallbacks.RESIZE_MODE_DOCKED_DIVIDER; import static android.view.WindowCallbacks.RESIZE_MODE_FREEFORM; import static android.view.WindowCallbacks.RESIZE_MODE_INVALID; +import static android.view.WindowInsets.Type.navigationBars; import static android.view.WindowInsets.Type.systemBars; import static android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE; import static android.view.WindowLayout.UNSPECIFIED_LENGTH; @@ -233,7 +233,6 @@ import android.view.InputWindowHandle; import android.view.InsetsSource; import android.view.InsetsState; import android.view.InsetsState.InternalInsetsType; -import android.view.InsetsVisibilities; import android.view.Surface; import android.view.Surface.Rotation; import android.view.SurfaceControl; @@ -767,7 +766,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP */ private boolean mIsDimming = false; - private final InsetsVisibilities mRequestedVisibilities = new InsetsVisibilities(); + private @InsetsType int mRequestedVisibleTypes = WindowInsets.Type.defaultVisible(); /** * Freeze the insets state in some cases that not necessarily keeps up-to-date to the client. @@ -833,31 +832,33 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP */ private int mSurfaceTranslationY; - /** - * Returns the visibility of the given {@link InternalInsetsType type} requested by the client. - * - * @param type the given {@link InternalInsetsType type}. - * @return {@code true} if the type is requested visible. - */ @Override - public boolean getRequestedVisibility(@InternalInsetsType int type) { - return mRequestedVisibilities.getVisibility(type); + public boolean isRequestedVisible(@InsetsType int types) { + return (mRequestedVisibleTypes & types) != 0; } /** - * Returns all the requested visibilities. + * Returns requested visible types of insets. * - * @return an {@link InsetsVisibilities} as the requested visibilities. + * @return an integer as the requested visible insets types. */ - InsetsVisibilities getRequestedVisibilities() { - return mRequestedVisibilities; + @Override + public @InsetsType int getRequestedVisibleTypes() { + return mRequestedVisibleTypes; } /** - * @see #getRequestedVisibility(int) + * @see #getRequestedVisibleTypes() */ - void setRequestedVisibilities(InsetsVisibilities visibilities) { - mRequestedVisibilities.set(visibilities); + void setRequestedVisibleTypes(@InsetsType int requestedVisibleTypes) { + if (mRequestedVisibleTypes != requestedVisibleTypes) { + mRequestedVisibleTypes = requestedVisibleTypes; + } + } + + @VisibleForTesting + void setRequestedVisibleTypes(@InsetsType int requestedVisibleTypes, @InsetsType int mask) { + setRequestedVisibleTypes(mRequestedVisibleTypes & ~mask | requestedVisibleTypes & mask); } /** @@ -973,7 +974,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP boolean isImplicitlyExcludingAllSystemGestures() { final boolean stickyHideNav = mAttrs.insetsFlags.behavior == BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE - && !getRequestedVisibility(ITYPE_NAVIGATION_BAR); + && !isRequestedVisible(navigationBars()); return stickyHideNav && mWmService.mConstants.mSystemGestureExcludedByPreQStickyImmersive && mActivityRecord != null && mActivityRecord.mTargetSdk < Build.VERSION_CODES.Q; } @@ -1718,7 +1719,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP InsetsState getInsetsStateWithVisibilityOverride() { final InsetsState state = new InsetsState(getInsetsState()); for (@InternalInsetsType int type = 0; type < InsetsState.SIZE; type++) { - final boolean requestedVisible = getRequestedVisibility(type); + final boolean requestedVisible = isRequestedVisible(InsetsState.toPublicType(type)); InsetsSource source = state.peekSource(type); if (source != null && source.isVisible() != requestedVisible) { source = new InsetsSource(source); @@ -4436,9 +4437,10 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP pw.println(prefix + "keepClearAreas: restricted=" + mKeepClearAreas + ", unrestricted=" + mUnrestrictedKeepClearAreas); if (dumpAll) { - final String visibilityString = mRequestedVisibilities.toString(); - if (!visibilityString.isEmpty()) { - pw.println(prefix + "Requested visibilities: " + visibilityString); + if (mRequestedVisibleTypes != WindowInsets.Type.defaultVisible()) { + pw.println(prefix + "Requested non-default-visibility types: " + + WindowInsets.Type.toString( + mRequestedVisibleTypes ^ WindowInsets.Type.defaultVisible())); } } diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp index 3f380e7914d0..0d872370dcdc 100644 --- a/services/core/jni/com_android_server_input_InputManagerService.cpp +++ b/services/core/jni/com_android_server_input_InputManagerService.cpp @@ -107,6 +107,7 @@ static struct { jmethodID notifyFocusChanged; jmethodID notifySensorEvent; jmethodID notifySensorAccuracy; + jmethodID notifyStylusGestureStarted; jmethodID notifyVibratorState; jmethodID filterInputEvent; jmethodID interceptKeyBeforeQueueing; @@ -299,6 +300,7 @@ public: void requestPointerCapture(const sp<IBinder>& windowToken, bool enabled); void setCustomPointerIcon(const SpriteIcon& icon); void setMotionClassifierEnabled(bool enabled); + std::optional<std::string> getBluetoothAddress(int32_t deviceId); /* --- InputReaderPolicyInterface implementation --- */ @@ -312,6 +314,7 @@ public: int32_t surfaceRotation) override; TouchAffineTransformation getTouchAffineTransformation(JNIEnv* env, jfloatArray matrixArr); + void notifyStylusGestureStarted(int32_t deviceId, nsecs_t eventTime) override; /* --- InputDispatcherPolicyInterface implementation --- */ @@ -370,37 +373,37 @@ private: Mutex mLock; struct Locked { // Display size information. - std::vector<DisplayViewport> viewports; + std::vector<DisplayViewport> viewports{}; // True if System UI is less noticeable. - bool systemUiLightsOut; + bool systemUiLightsOut{false}; // Pointer speed. - int32_t pointerSpeed; + int32_t pointerSpeed{0}; // Pointer acceleration. - float pointerAcceleration; + float pointerAcceleration{android::os::IInputConstants::DEFAULT_POINTER_ACCELERATION}; // True if pointer gestures are enabled. - bool pointerGesturesEnabled; + bool pointerGesturesEnabled{true}; // Show touches feature enable/disable. - bool showTouches; + bool showTouches{false}; // The latest request to enable or disable Pointer Capture. - PointerCaptureRequest pointerCaptureRequest; + PointerCaptureRequest pointerCaptureRequest{}; // Sprite controller singleton, created on first use. - sp<SpriteController> spriteController; + sp<SpriteController> spriteController{}; // Pointer controller singleton, created and destroyed as needed. - std::weak_ptr<PointerController> pointerController; + std::weak_ptr<PointerController> pointerController{}; // Input devices to be disabled - std::set<int32_t> disabledInputDevices; + std::set<int32_t> disabledInputDevices{}; // Associated Pointer controller display. - int32_t pointerDisplayId; + int32_t pointerDisplayId{ADISPLAY_ID_DEFAULT}; } mLocked GUARDED_BY(mLock); std::atomic<bool> mInteractive; @@ -419,16 +422,6 @@ NativeInputManager::NativeInputManager(jobject serviceObj, const sp<Looper>& loo mServiceObj = env->NewGlobalRef(serviceObj); - { - AutoMutex _l(mLock); - mLocked.systemUiLightsOut = false; - mLocked.pointerSpeed = 0; - mLocked.pointerAcceleration = android::os::IInputConstants::DEFAULT_POINTER_ACCELERATION; - mLocked.pointerGesturesEnabled = true; - mLocked.showTouches = false; - mLocked.pointerDisplayId = ADISPLAY_ID_DEFAULT; - } - mInteractive = true; InputManager* im = new InputManager(this, this); mInputManager = im; defaultServiceManager()->addService(String16("inputflinger"), im); @@ -1177,6 +1170,13 @@ TouchAffineTransformation NativeInputManager::getTouchAffineTransformation( return transform; } +void NativeInputManager::notifyStylusGestureStarted(int32_t deviceId, nsecs_t eventTime) { + JNIEnv* env = jniEnv(); + env->CallVoidMethod(mServiceObj, gServiceClassInfo.notifyStylusGestureStarted, deviceId, + eventTime); + checkAndClearExceptionFromCallback(env, "notifyStylusGestureStarted"); +} + bool NativeInputManager::filterInputEvent(const InputEvent* inputEvent, uint32_t policyFlags) { ATRACE_CALL(); jobject inputEventObj; @@ -1487,6 +1487,10 @@ void NativeInputManager::setMotionClassifierEnabled(bool enabled) { mInputManager->getProcessor().setMotionClassifierEnabled(enabled); } +std::optional<std::string> NativeInputManager::getBluetoothAddress(int32_t deviceId) { + return mInputManager->getReader().getBluetoothAddress(deviceId); +} + bool NativeInputManager::isPerDisplayTouchModeEnabled() { JNIEnv* env = jniEnv(); jboolean enabled = @@ -2326,6 +2330,12 @@ static void nativeSetPointerDisplayId(JNIEnv* env, jobject nativeImplObj, jint d im->setPointerDisplayId(displayId); } +static jstring nativeGetBluetoothAddress(JNIEnv* env, jobject nativeImplObj, jint deviceId) { + NativeInputManager* im = getNativeInputManager(env, nativeImplObj); + const auto address = im->getBluetoothAddress(deviceId); + return address ? env->NewStringUTF(address->c_str()) : nullptr; +} + // ---------------------------------------------------------------------------- static const JNINativeMethod gInputManagerMethods[] = { @@ -2408,6 +2418,7 @@ static const JNINativeMethod gInputManagerMethods[] = { {"flushSensor", "(II)Z", (void*)nativeFlushSensor}, {"cancelCurrentTouch", "()V", (void*)nativeCancelCurrentTouch}, {"setPointerDisplayId", "(I)V", (void*)nativeSetPointerDisplayId}, + {"getBluetoothAddress", "(I)Ljava/lang/String;", (void*)nativeGetBluetoothAddress}, }; #define FIND_CLASS(var, className) \ @@ -2469,6 +2480,9 @@ int register_android_server_InputManager(JNIEnv* env) { GET_METHOD_ID(gServiceClassInfo.notifySensorAccuracy, clazz, "notifySensorAccuracy", "(III)V"); + GET_METHOD_ID(gServiceClassInfo.notifyStylusGestureStarted, clazz, "notifyStylusGestureStarted", + "(IJ)V"); + GET_METHOD_ID(gServiceClassInfo.notifyVibratorState, clazz, "notifyVibratorState", "(IZ)V"); GET_METHOD_ID(gServiceClassInfo.notifyNoFocusedWindowAnr, clazz, "notifyNoFocusedWindowAnr", diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java index 91f5c6999427..40412db2ed5e 100644 --- a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java +++ b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java @@ -21,20 +21,28 @@ import static android.content.Context.CREDENTIAL_SERVICE; import android.annotation.NonNull; import android.annotation.UserIdInt; import android.content.Context; +import android.content.pm.PackageManager; import android.credentials.CreateCredentialRequest; import android.credentials.GetCredentialRequest; import android.credentials.ICreateCredentialCallback; import android.credentials.ICredentialManager; import android.credentials.IGetCredentialCallback; +import android.os.Binder; import android.os.CancellationSignal; import android.os.ICancellationSignal; import android.os.UserHandle; import android.provider.Settings; +import android.text.TextUtils; import android.util.Log; +import android.util.Slog; import com.android.server.infra.AbstractMasterSystemService; import com.android.server.infra.SecureSettingsServiceNameResolver; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + /** * Entry point service for credential management. * @@ -49,52 +57,99 @@ public final class CredentialManagerService extends public CredentialManagerService(@NonNull Context context) { super(context, - new SecureSettingsServiceNameResolver(context, Settings.Secure.AUTOFILL_SERVICE), + new SecureSettingsServiceNameResolver(context, Settings.Secure.CREDENTIAL_SERVICE, + /*isMultipleMode=*/true), null, PACKAGE_UPDATE_POLICY_REFRESH_EAGER); } @Override protected String getServiceSettingsProperty() { - return Settings.Secure.AUTOFILL_SERVICE; + return Settings.Secure.CREDENTIAL_SERVICE; } @Override // from AbstractMasterSystemService protected CredentialManagerServiceImpl newServiceLocked(@UserIdInt int resolvedUserId, boolean disabled) { - return new CredentialManagerServiceImpl(this, mLock, resolvedUserId); + // This method should not be called for CredentialManagerService as it is configured to use + // multiple services. + Slog.w(TAG, "Should not be here - CredentialManagerService is configured to use " + + "multiple services"); + return null; } - @Override + @Override // from SystemService public void onStart() { - Log.i(TAG, "onStart"); publishBinderService(CREDENTIAL_SERVICE, new CredentialManagerServiceStub()); } + @Override // from AbstractMasterSystemService + protected List<CredentialManagerServiceImpl> newServiceListLocked(int resolvedUserId, + boolean disabled, String[] serviceNames) { + if (serviceNames == null || serviceNames.length == 0) { + Slog.i(TAG, "serviceNames sent in newServiceListLocked is null, or empty"); + return new ArrayList<>(); + } + List<CredentialManagerServiceImpl> serviceList = new ArrayList<>(serviceNames.length); + for (int i = 0; i < serviceNames.length; i++) { + Log.i(TAG, "in newServiceListLocked, service: " + serviceNames[i]); + if (TextUtils.isEmpty(serviceNames[i])) { + continue; + } + try { + serviceList.add(new CredentialManagerServiceImpl(this, mLock, resolvedUserId, + serviceNames[i])); + } catch (PackageManager.NameNotFoundException e) { + Log.i(TAG, "Unable to add serviceInfo : " + e.getMessage()); + } catch (SecurityException e) { + Log.i(TAG, "Unable to add serviceInfo : " + e.getMessage()); + } + } + return serviceList; + } + + private void runForUser(@NonNull final Consumer<CredentialManagerServiceImpl> c) { + final int userId = UserHandle.getCallingUserId(); + final long origId = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + final List<CredentialManagerServiceImpl> services = + getServiceListForUserLocked(userId); + services.forEach(s -> { + c.accept(s); + }); + } + } finally { + Binder.restoreCallingIdentity(origId); + } + } + final class CredentialManagerServiceStub extends ICredentialManager.Stub { @Override public ICancellationSignal executeGetCredential( GetCredentialRequest request, - IGetCredentialCallback callback) { - // TODO: implement. - Log.i(TAG, "executeGetCredential"); + IGetCredentialCallback callback, + final String callingPackage) { + Log.i(TAG, "starting executeGetCredential with callingPackage: " + callingPackage); + // TODO : Implement cancellation + ICancellationSignal cancelTransport = CancellationSignal.createTransport(); - final int userId = UserHandle.getCallingUserId(); - synchronized (mLock) { - final CredentialManagerServiceImpl service = peekServiceForUserLocked(userId); - if (service != null) { - Log.i(TAG, "Got service for : " + userId); - service.getCredential(); - } - } + // New request session, scoped for this request only. + final GetRequestSession session = new GetRequestSession(getContext(), + UserHandle.getCallingUserId(), + callback); - ICancellationSignal cancelTransport = CancellationSignal.createTransport(); + // Invoke all services of a user + runForUser((service) -> { + service.getCredential(request, session, callingPackage); + }); return cancelTransport; } @Override public ICancellationSignal executeCreateCredential( CreateCredentialRequest request, - ICreateCredentialCallback callback) { + ICreateCredentialCallback callback, + String callingPackage) { // TODO: implement. Log.i(TAG, "executeCreateCredential"); ICancellationSignal cancelTransport = CancellationSignal.createTransport(); diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java b/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java index aa19241e77dd..cc03f9b89119 100644 --- a/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java +++ b/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java @@ -17,47 +17,94 @@ package com.android.server.credentials; import android.annotation.NonNull; -import android.app.AppGlobals; +import android.annotation.Nullable; import android.content.ComponentName; import android.content.pm.PackageManager; import android.content.pm.ServiceInfo; -import android.os.RemoteException; -import android.util.Log; +import android.credentials.GetCredentialRequest; +import android.service.credentials.CredentialProviderInfo; +import android.service.credentials.GetCredentialsRequest; +import android.util.Slog; import com.android.server.infra.AbstractPerUserSystemService; + /** - * Per-user implementation of {@link CredentialManagerService} + * Per-user, per remote service implementation of {@link CredentialManagerService} */ public final class CredentialManagerServiceImpl extends AbstractPerUserSystemService<CredentialManagerServiceImpl, CredentialManagerService> { private static final String TAG = "CredManSysServiceImpl"; - protected CredentialManagerServiceImpl( + // TODO(b/210531) : Make final when update flow is fixed + private ComponentName mRemoteServiceComponentName; + private CredentialProviderInfo mInfo; + + public CredentialManagerServiceImpl( @NonNull CredentialManagerService master, - @NonNull Object lock, int userId) { + @NonNull Object lock, int userId, String serviceName) + throws PackageManager.NameNotFoundException { super(master, lock, userId); + Slog.i(TAG, "in CredentialManagerServiceImpl cons"); + // TODO : Replace with newServiceInfoLocked after confirming behavior + mRemoteServiceComponentName = ComponentName.unflattenFromString(serviceName); + mInfo = new CredentialProviderInfo(getContext(), mRemoteServiceComponentName, mUserId); } @Override // from PerUserSystemService protected ServiceInfo newServiceInfoLocked(@NonNull ComponentName serviceComponent) throws PackageManager.NameNotFoundException { - ServiceInfo si; - try { - si = AppGlobals.getPackageManager().getServiceInfo(serviceComponent, - PackageManager.GET_META_DATA, mUserId); - } catch (RemoteException e) { - throw new PackageManager.NameNotFoundException( - "Could not get service for " + serviceComponent); + // TODO : Test update flows with multiple providers + Slog.i(TAG , "newServiceInfoLocked with : " + serviceComponent.getPackageName()); + mRemoteServiceComponentName = serviceComponent; + mInfo = new CredentialProviderInfo(getContext(), serviceComponent, mUserId); + return mInfo.getServiceInfo(); + } + + public void getCredential(GetCredentialRequest request, GetRequestSession requestSession, + String callingPackage) { + Slog.i(TAG, "in getCredential in CredManServiceImpl"); + if (mInfo == null) { + Slog.i(TAG, "in getCredential in CredManServiceImpl, but mInfo is null"); + return; + } + + // TODO : Determine if remoteService instance can be reused across requests + final RemoteCredentialService remoteService = new RemoteCredentialService( + getContext(), mInfo.getServiceInfo().getComponentName(), mUserId); + ProviderGetSession providerSession = new ProviderGetSession(mInfo, + requestSession, mUserId, remoteService); + // Set the provider info to the session when the request is initiated. This happens here + // because there is one serviceImpl per remote provider, and so we can only retrieve + // the provider information in the scope of this instance, whereas the session is for the + // entire request. + requestSession.addProviderSession(providerSession); + GetCredentialsRequest filteredRequest = getRequestWithValidType(request, callingPackage); + if (filteredRequest != null) { + remoteService.onGetCredentials(getRequestWithValidType(request, callingPackage), + providerSession); } - return si; } - /** - * Unimplemented getCredentials - */ - public void getCredential() { - Log.i(TAG, "getCredential not implemented"); - // TODO : Implement logic + @Nullable + private GetCredentialsRequest getRequestWithValidType(GetCredentialRequest request, + String callingPackage) { + GetCredentialsRequest.Builder builder = + new GetCredentialsRequest.Builder(callingPackage); + request.getGetCredentialOptions().forEach( option -> { + if (mInfo.hasCapability(option.getType())) { + Slog.i(TAG, "Provider can handle: " + option.getType()); + builder.addGetCredentialOption(option); + } else { + Slog.i(TAG, "Skipping request as provider cannot handle it"); + } + }); + + try { + return builder.build(); + } catch (IllegalArgumentException | NullPointerException e) { + Slog.i(TAG, "issue with request build: " + e.getMessage()); + } + return null; } } diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java b/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java new file mode 100644 index 000000000000..69fb1eaa7543 --- /dev/null +++ b/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.credentials; + +import android.annotation.NonNull; +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.credentials.ui.IntentFactory; +import android.credentials.ui.ProviderData; +import android.credentials.ui.RequestInfo; +import android.credentials.ui.UserSelectionDialogResult; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.ResultReceiver; +import android.util.Log; +import android.util.Slog; + +import java.util.ArrayList; + +/** Initiates the Credential Manager UI and receives results. */ +public class CredentialManagerUi { + private static final String TAG = "CredentialManagerUi"; + @NonNull + private final CredentialManagerUiCallback mCallbacks; + @NonNull private final Context mContext; + private final int mUserId; + @NonNull private final ResultReceiver mResultReceiver = new ResultReceiver( + new Handler(Looper.getMainLooper())) { + @Override + protected void onReceiveResult(int resultCode, Bundle resultData) { + handleUiResult(resultCode, resultData); + } + }; + + private void handleUiResult(int resultCode, Bundle resultData) { + if (resultCode == Activity.RESULT_OK) { + UserSelectionDialogResult selection = UserSelectionDialogResult + .fromResultData(resultData); + if (selection != null) { + mCallbacks.onUiSelection(selection); + } else { + Slog.i(TAG, "No selection found in UI result"); + } + } else if (resultCode == Activity.RESULT_CANCELED) { + mCallbacks.onUiCancelation(); + } + } + + /** + * Interface to be implemented by any class that wishes to get callbacks from the UI. + */ + public interface CredentialManagerUiCallback { + /** Called when the user makes a selection. */ + void onUiSelection(UserSelectionDialogResult selection); + /** Called when the user cancels the UI. */ + void onUiCancelation(); + } + public CredentialManagerUi(Context context, int userId, + CredentialManagerUiCallback callbacks) { + Log.i(TAG, "In CredentialManagerUi constructor"); + mContext = context; + mUserId = userId; + mCallbacks = callbacks; + } + + /** + * Surfaces the Credential Manager bottom sheet UI. + * @param providerDataList the list of provider data from remote providers + */ + public void show(RequestInfo requestInfo, ArrayList<ProviderData> providerDataList) { + Log.i(TAG, "In show"); + Intent intent = IntentFactory.newIntent(requestInfo, providerDataList, + mResultReceiver); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + mContext.startActivity(intent); + } +} diff --git a/services/credentials/java/com/android/server/credentials/GetRequestSession.java b/services/credentials/java/com/android/server/credentials/GetRequestSession.java new file mode 100644 index 000000000000..80f0fec06825 --- /dev/null +++ b/services/credentials/java/com/android/server/credentials/GetRequestSession.java @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.credentials; + +import android.content.ComponentName; +import android.content.Context; +import android.credentials.Credential; +import android.credentials.GetCredentialResponse; +import android.credentials.IGetCredentialCallback; +import android.credentials.ui.ProviderData; +import android.credentials.ui.RequestInfo; +import android.credentials.ui.UserSelectionDialogResult; +import android.os.RemoteException; +import android.service.credentials.CredentialEntry; +import android.util.Log; +import android.util.Slog; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Central session for a single getCredentials request. This class listens to the + * responses from providers, and the UX app, and updates the provider(S) state. + */ +public final class GetRequestSession extends RequestSession { + private static final String TAG = "GetRequestSession"; + + private final IGetCredentialCallback mClientCallback; + private final Map<String, ProviderGetSession> mProviders; + + public GetRequestSession(Context context, int userId, + IGetCredentialCallback callback) { + super(context, userId, RequestInfo.TYPE_GET); + mClientCallback = callback; + mProviders = new HashMap<>(); + } + + /** + * Adds a new provider to the list of providers that are contributing to this session. + */ + public void addProviderSession(ProviderGetSession providerSession) { + mProviders.put(providerSession.getComponentName().flattenToString(), + providerSession); + } + + @Override + public void onProviderStatusChanged(ProviderSession.Status status, + ComponentName componentName) { + Log.i(TAG, "in onStatusChanged"); + if (ProviderSession.isTerminatingStatus(status)) { + Log.i(TAG, "in onStatusChanged terminating status"); + + ProviderGetSession session = mProviders.remove(componentName.flattenToString()); + if (session != null) { + Slog.i(TAG, "Provider session removed."); + } else { + Slog.i(TAG, "Provider session null, did not exist."); + } + } else if (ProviderSession.isCompletionStatus(status)) { + Log.i(TAG, "in onStatusChanged isCompletionStatus status"); + onProviderResponseComplete(); + } + } + + @Override + public void onUiSelection(UserSelectionDialogResult selection) { + String providerId = selection.getProviderId(); + ProviderGetSession providerSession = mProviders.get(providerId); + if (providerSession != null) { + CredentialEntry credentialEntry = providerSession.getCredentialEntry( + selection.getEntrySubkey()); + if (credentialEntry != null && credentialEntry.getCredential() != null) { + respondToClientAndFinish(credentialEntry.getCredential()); + } + // TODO : Handle action chips and authentication selection + return; + } + // TODO : finish session and respond to client if provider not found + } + + @Override + public void onUiCancelation() { + // User canceled the activity + // TODO : Send error code to client + finishSession(); + } + + private void onProviderResponseComplete() { + Log.i(TAG, "in onProviderResponseComplete"); + if (isResponseCompleteAcrossProviders()) { + Log.i(TAG, "in onProviderResponseComplete - isResponseCompleteAcrossProviders"); + getProviderDataAndInitiateUi(); + } + } + + private void getProviderDataAndInitiateUi() { + ArrayList<ProviderData> providerDataList = new ArrayList<>(); + for (ProviderGetSession session : mProviders.values()) { + Log.i(TAG, "preparing data for : " + session.getComponentName()); + providerDataList.add(session.prepareUiData()); + } + if (!providerDataList.isEmpty()) { + Log.i(TAG, "provider list not empty about to initiate ui"); + initiateUi(providerDataList); + } + } + + private void initiateUi(ArrayList<ProviderData> providerDataList) { + mHandler.post(() -> mCredentialManagerUi.show(RequestInfo.newGetRequestInfo( + mRequestId, null, mIsFirstUiTurn, ""), + providerDataList)); + } + + /** + * Iterates over all provider sessions and returns true if all have responded. + */ + private boolean isResponseCompleteAcrossProviders() { + AtomicBoolean isRequestComplete = new AtomicBoolean(true); + mProviders.forEach( (packageName, session) -> { + if (session.getStatus() != ProviderSession.Status.COMPLETE) { + isRequestComplete.set(false); + } + }); + return isRequestComplete.get(); + } + + private void respondToClientAndFinish(Credential credential) { + try { + mClientCallback.onResponse(new GetCredentialResponse(credential)); + } catch (RemoteException e) { + e.printStackTrace(); + } + finishSession(); + } + + private void finishSession() { + clearProviderSessions(); + } + + private void clearProviderSessions() { + for (ProviderGetSession session : mProviders.values()) { + // TODO : Evaluate if we should unbind remote services here or wait for them + // to automatically unbind when idle. Re-binding frequently also has a cost. + //session.destroy(); + } + mProviders.clear(); + } +} diff --git a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java new file mode 100644 index 000000000000..24610df7ad79 --- /dev/null +++ b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.credentials; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.slice.Slice; +import android.credentials.ui.Entry; +import android.credentials.ui.ProviderData; +import android.service.credentials.Action; +import android.service.credentials.CredentialEntry; +import android.service.credentials.CredentialProviderInfo; +import android.service.credentials.CredentialsDisplayContent; +import android.service.credentials.GetCredentialsResponse; +import android.util.Log; +import android.util.Slog; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +/** + * Central provider session that listens for provider callbacks, and maintains provider state. + * Will likely split this into remote response state and UI state. + */ +public final class ProviderGetSession extends ProviderSession<GetCredentialsResponse> + implements RemoteCredentialService.ProviderCallbacks<GetCredentialsResponse> { + private static final String TAG = "ProviderGetSession"; + + // Key to be used as an entry key for a credential entry + private static final String CREDENTIAL_ENTRY_KEY = "credential_key"; + + private GetCredentialsResponse mResponse; + + @NonNull + private final Map<String, CredentialEntry> mUiCredentials = new HashMap<>(); + + @NonNull + private final Map<String, Action> mUiActions = new HashMap<>(); + + public ProviderGetSession(CredentialProviderInfo info, + ProviderInternalCallback callbacks, + int userId, RemoteCredentialService remoteCredentialService) { + super(info, callbacks, userId, remoteCredentialService); + setStatus(Status.PENDING); + } + + /** Updates the response being maintained in state by this provider session. */ + @Override + public void updateResponse(GetCredentialsResponse response) { + if (response.getAuthenticationAction() != null) { + // TODO : Implement authentication logic + } else if (response.getCredentialsDisplayContent() != null) { + Log.i(TAG , "updateResponse with credentialEntries"); + mResponse = response; + updateStatusAndInvokeCallback(Status.COMPLETE); + } + } + + /** Returns the response being maintained in this provider session. */ + @Override + @Nullable + public GetCredentialsResponse getResponse() { + return mResponse; + } + + /** Returns the credential entry maintained in state by this provider session. */ + @Nullable + public CredentialEntry getCredentialEntry(@NonNull String entryId) { + return mUiCredentials.get(entryId); + } + + /** Returns the action entry maintained in state by this provider session. */ + @Nullable + public Action getAction(@NonNull String entryId) { + return mUiActions.get(entryId); + } + + /** Called when the provider response has been updated by an external source. */ + @Override + public void onProviderResponseSuccess(@Nullable GetCredentialsResponse response) { + Log.i(TAG, "in onProviderResponseSuccess"); + updateResponse(response); + } + + /** Called when the provider response resulted in a failure. */ + @Override + public void onProviderResponseFailure(int errorCode, @Nullable CharSequence message) { + updateStatusAndInvokeCallback(toStatus(errorCode)); + } + + /** Called when provider service dies. */ + @Override + public void onProviderServiceDied(RemoteCredentialService service) { + if (service.getComponentName().equals(mProviderInfo.getServiceInfo().getComponentName())) { + updateStatusAndInvokeCallback(Status.SERVICE_DEAD); + } else { + Slog.i(TAG, "Component names different in onProviderServiceDied - " + + "this should not happen"); + } + } + + @Override + protected final ProviderData prepareUiData() throws IllegalArgumentException { + Log.i(TAG, "In prepareUiData"); + if (!ProviderSession.isCompletionStatus(getStatus())) { + Log.i(TAG, "In prepareUiData not complete"); + + throw new IllegalStateException("Status must be in completion mode"); + } + GetCredentialsResponse response = getResponse(); + if (response == null) { + Log.i(TAG, "In prepareUiData response null"); + + throw new IllegalStateException("Response must be in completion mode"); + } + if (response.getAuthenticationAction() != null) { + Log.i(TAG, "In prepareUiData auth not null"); + + return prepareUiProviderDataWithAuthentication(response.getAuthenticationAction()); + } + if (response.getCredentialsDisplayContent() != null){ + Log.i(TAG, "In prepareUiData credentials not null"); + + return prepareUiProviderDataWithCredentials(response.getCredentialsDisplayContent()); + } + return null; + } + + /** + * To be called by {@link ProviderGetSession} when the UI is to be invoked. + */ + @Nullable + private ProviderData prepareUiProviderDataWithCredentials(@NonNull + CredentialsDisplayContent content) { + Log.i(TAG, "in prepareUiProviderData"); + List<Entry> credentialEntries = new ArrayList<>(); + List<Entry> actionChips = new ArrayList<>(); + Entry authenticationEntry = null; + + // Populate the credential entries + for (CredentialEntry credentialEntry : content.getCredentialEntries()) { + String entryId = UUID.randomUUID().toString(); + mUiCredentials.put(entryId, credentialEntry); + Log.i(TAG, "in prepareUiProviderData creating ui entry with id " + entryId); + Slice slice = credentialEntry.getSlice(); + // TODO : Remove conversion of string to int after change in Entry class + credentialEntries.add(new Entry(CREDENTIAL_ENTRY_KEY, entryId, + credentialEntry.getSlice())); + } + // populate the action chip + for (Action action : content.getActions()) { + String entryId = UUID.randomUUID().toString(); + mUiActions.put(entryId, action); + // TODO : Remove conversion of string to int after change in Entry class + actionChips.add(new Entry(ACTION_ENTRY_KEY, entryId, + action.getSlice())); + } + + // TODO : Set the correct last used time + return new ProviderData.Builder(mComponentName.flattenToString(), + mProviderInfo.getServiceLabel() == null ? "" : + mProviderInfo.getServiceLabel().toString(), + /*icon=*/null) + .setCredentialEntries(credentialEntries) + .setActionChips(actionChips) + .setAuthenticationEntry(authenticationEntry) + .setLastUsedTimeMillis(0) + .build(); + } + + /** + * To be called by {@link ProviderGetSession} when the UI is to be invoked. + */ + @Nullable + private ProviderData prepareUiProviderDataWithAuthentication(@NonNull + Action authenticationEntry) { + // TODO : Implement authentication flow + return null; + } +} diff --git a/services/credentials/java/com/android/server/credentials/ProviderSession.java b/services/credentials/java/com/android/server/credentials/ProviderSession.java new file mode 100644 index 000000000000..3a9f96432d8c --- /dev/null +++ b/services/credentials/java/com/android/server/credentials/ProviderSession.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.credentials; + +import android.annotation.NonNull; +import android.content.ComponentName; +import android.credentials.ui.ProviderData; +import android.service.credentials.CredentialProviderException; +import android.service.credentials.CredentialProviderInfo; + +/** + * Provider session storing the state of provider response and ui entries. + * @param <T> The request type expected from the remote provider, for a given request session. + */ +public abstract class ProviderSession<T> implements RemoteCredentialService.ProviderCallbacks<T> { + // Key to be used as the entry key for an action entry + protected static final String ACTION_ENTRY_KEY = "action_key"; + + @NonNull protected final ComponentName mComponentName; + @NonNull protected final CredentialProviderInfo mProviderInfo; + @NonNull protected final RemoteCredentialService mRemoteCredentialService; + @NonNull protected final int mUserId; + @NonNull protected Status mStatus = Status.NOT_STARTED; + @NonNull protected final ProviderInternalCallback mCallbacks; + + /** + * Interface to be implemented by any class that wishes to get a callback when a particular + * provider session's status changes. Typically, implemented by the {@link RequestSession} + * class. + */ + public interface ProviderInternalCallback { + /** + * Called when status changes. + */ + void onProviderStatusChanged(Status status, ComponentName componentName); + } + + protected ProviderSession(@NonNull CredentialProviderInfo info, + @NonNull ProviderInternalCallback callbacks, + @NonNull int userId, + @NonNull RemoteCredentialService remoteCredentialService) { + mProviderInfo = info; + mCallbacks = callbacks; + mUserId = userId; + mComponentName = info.getServiceInfo().getComponentName(); + mRemoteCredentialService = remoteCredentialService; + } + + /** Update the response state stored with the provider session. */ + protected abstract void updateResponse (T response); + + /** Update the response state stored with the provider session. */ + protected abstract T getResponse (); + + /** Should be overridden to prepare, and stores state for {@link ProviderData} to be + * shown on the UI. */ + protected abstract ProviderData prepareUiData(); + + /** Provider status at various states of the request session. */ + enum Status { + NOT_STARTED, + PENDING, + REQUIRES_AUTHENTICATION, + COMPLETE, + SERVICE_DEAD, + CANCELED + } + + protected void setStatus(@NonNull Status status) { + mStatus = status; + } + + @NonNull + protected Status getStatus() { + return mStatus; + } + + @NonNull + protected ComponentName getComponentName() { + return mComponentName; + } + + /** Updates the status .*/ + protected void updateStatusAndInvokeCallback(@NonNull Status status) { + setStatus(status); + mCallbacks.onProviderStatusChanged(status, mComponentName); + } + + @NonNull + public static Status toStatus( + @CredentialProviderException.CredentialProviderError int errorCode) { + // TODO : Add more mappings as more flows are supported + return Status.CANCELED; + } + + /** + * Returns true if the given status means that the provider session must be terminated. + */ + public static boolean isTerminatingStatus(Status status) { + return status == Status.CANCELED || status == Status.SERVICE_DEAD; + } + + /** + * Returns true if the given status means that the provider is done getting the response, + * and is ready for user interaction. + */ + public static boolean isCompletionStatus(Status status) { + return status == Status.COMPLETE || status == Status.REQUIRES_AUTHENTICATION; + } +} diff --git a/services/credentials/java/com/android/server/credentials/RemoteCredentialService.java b/services/credentials/java/com/android/server/credentials/RemoteCredentialService.java new file mode 100644 index 000000000000..d0b6e7d6238c --- /dev/null +++ b/services/credentials/java/com/android/server/credentials/RemoteCredentialService.java @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.credentials; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.os.Handler; +import android.os.ICancellationSignal; +import android.os.RemoteException; +import android.service.credentials.CredentialProviderException; +import android.service.credentials.CredentialProviderException.CredentialProviderError; +import android.service.credentials.CredentialProviderService; +import android.service.credentials.GetCredentialsRequest; +import android.service.credentials.GetCredentialsResponse; +import android.service.credentials.ICredentialProviderService; +import android.service.credentials.IGetCredentialsCallback; +import android.text.format.DateUtils; +import android.util.Log; +import android.util.Slog; + +import com.android.internal.infra.ServiceConnector; + +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Handles connections with the remote credential provider + * + * @hide + */ +public class RemoteCredentialService extends ServiceConnector.Impl<ICredentialProviderService>{ + + private static final String TAG = "RemoteCredentialService"; + /** Timeout for a single request. */ + private static final long TIMEOUT_REQUEST_MILLIS = 5 * DateUtils.SECOND_IN_MILLIS; + /** Timeout to unbind after the task queue is empty. */ + private static final long TIMEOUT_IDLE_SERVICE_CONNECTION_MILLIS = + 5 * DateUtils.SECOND_IN_MILLIS; + + private final ComponentName mComponentName; + + /** + * Callbacks to be invoked when the provider remote service responds with a + * success or failure. + * @param <T> the type of response expected from the provider + */ + public interface ProviderCallbacks<T> { + /** Called when a successful response is received from the remote provider. */ + void onProviderResponseSuccess(@Nullable T response); + /** Called when a failure response is received from the remote provider. */ + void onProviderResponseFailure(int errorCode, @Nullable CharSequence message); + /** Called when the remote provider service dies. */ + void onProviderServiceDied(RemoteCredentialService service); + } + + public RemoteCredentialService(@NonNull Context context, + @NonNull ComponentName componentName, int userId) { + super(context, new Intent(CredentialProviderService.SERVICE_INTERFACE) + .setComponent(componentName), Context.BIND_ALLOW_BACKGROUND_ACTIVITY_STARTS, + userId, ICredentialProviderService.Stub::asInterface); + mComponentName = componentName; + } + + /** Unbinds automatically after this amount of time. */ + @Override + protected long getAutoDisconnectTimeoutMs() { + return TIMEOUT_IDLE_SERVICE_CONNECTION_MILLIS; + } + + /** Return the componentName of the service to be connected. */ + @NonNull public ComponentName getComponentName() { + return mComponentName; + } + + /** Destroys this remote service by unbinding the connection. */ + public void destroy() { + unbind(); + } + + /** Main entry point to be called for executing a getCredential call on the remote + * provider service. + * @param request the request to be sent to the provider + * @param callback the callback to be used to send back the provider response to the + * {@link ProviderSession} class that maintains provider state + */ + public void onGetCredentials(@NonNull GetCredentialsRequest request, + ProviderCallbacks<GetCredentialsResponse> callback) { + Log.i(TAG, "In onGetCredentials in RemoteCredentialService"); + AtomicReference<ICancellationSignal> cancellationSink = new AtomicReference<>(); + AtomicReference<CompletableFuture<GetCredentialsResponse>> futureRef = + new AtomicReference<>(); + + CompletableFuture<GetCredentialsResponse> connectThenExecute = postAsync(service -> { + CompletableFuture<GetCredentialsResponse> getCredentials = new CompletableFuture<>(); + ICancellationSignal cancellationSignal = + service.onGetCredentials(request, new IGetCredentialsCallback.Stub() { + @Override + public void onSuccess(GetCredentialsResponse response) { + Log.i(TAG, "In onSuccess in RemoteCredentialService"); + getCredentials.complete(response); + } + + @Override + public void onFailure(@CredentialProviderError int errorCode, + CharSequence message) { + Log.i(TAG, "In onFailure in RemoteCredentialService"); + String errorMsg = message == null ? "" : String.valueOf(message); + getCredentials.completeExceptionally(new CredentialProviderException( + errorCode, errorMsg)); + } + }); + CompletableFuture<GetCredentialsResponse> future = futureRef.get(); + if (future != null && future.isCancelled()) { + dispatchCancellationSignal(cancellationSignal); + } else { + cancellationSink.set(cancellationSignal); + } + return getCredentials; + }).orTimeout(TIMEOUT_REQUEST_MILLIS, TimeUnit.MILLISECONDS); + futureRef.set(connectThenExecute); + + connectThenExecute.whenComplete((result, error) -> Handler.getMain().post(() -> { + if (error == null) { + Log.i(TAG, "In RemoteCredentialService execute error is null"); + callback.onProviderResponseSuccess(result); + } else { + if (error instanceof TimeoutException) { + Log.i(TAG, "In RemoteCredentialService execute error is timeout"); + dispatchCancellationSignal(cancellationSink.get()); + callback.onProviderResponseFailure( + CredentialProviderException.ERROR_TIMEOUT, + error.getMessage()); + } else if (error instanceof CancellationException) { + Log.i(TAG, "In RemoteCredentialService execute error is cancellation"); + dispatchCancellationSignal(cancellationSink.get()); + callback.onProviderResponseFailure( + CredentialProviderException.ERROR_TASK_CANCELED, + error.getMessage()); + } else if (error instanceof CredentialProviderException) { + Log.i(TAG, "In RemoteCredentialService execute error is provider error"); + callback.onProviderResponseFailure(((CredentialProviderException) error) + .getErrorCode(), + error.getMessage()); + } else { + Log.i(TAG, "In RemoteCredentialService execute error is unknown"); + callback.onProviderResponseFailure( + CredentialProviderException.ERROR_UNKNOWN, + error.getMessage()); + } + } + })); + } + + private void dispatchCancellationSignal(@Nullable ICancellationSignal signal) { + if (signal == null) { + Slog.e(TAG, "Error dispatching a cancellation - Signal is null"); + return; + } + try { + signal.cancel(); + } catch (RemoteException e) { + Slog.e(TAG, "Error dispatching a cancellation", e); + } + } +} diff --git a/services/credentials/java/com/android/server/credentials/RequestSession.java b/services/credentials/java/com/android/server/credentials/RequestSession.java new file mode 100644 index 000000000000..1bacbb342edb --- /dev/null +++ b/services/credentials/java/com/android/server/credentials/RequestSession.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.credentials; + +import android.annotation.NonNull; +import android.annotation.UserIdInt; +import android.content.ComponentName; +import android.content.Context; +import android.credentials.ui.UserSelectionDialogResult; +import android.os.Binder; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; + +/** + * Base class of a request session, that listens to UI events. This class must be extended + * every time a new response type is expected from the providers. + */ +abstract class RequestSession implements CredentialManagerUi.CredentialManagerUiCallback, + ProviderSession.ProviderInternalCallback { + @NonNull protected final IBinder mRequestId; + @NonNull protected final Context mContext; + @NonNull protected final CredentialManagerUi mCredentialManagerUi; + @NonNull protected final String mRequestType; + @NonNull protected final Handler mHandler; + @NonNull protected boolean mIsFirstUiTurn = true; + @UserIdInt protected final int mUserId; + + protected RequestSession(@NonNull Context context, + @UserIdInt int userId, @NonNull String requestType) { + mContext = context; + mUserId = userId; + mRequestType = requestType; + mHandler = new Handler(Looper.getMainLooper(), null, true); + mRequestId = new Binder(); + mCredentialManagerUi = new CredentialManagerUi(mContext, + mUserId, this); + } + + /** Returns the unique identifier of this request session. */ + public IBinder getRequestId() { + return mRequestId; + } + + @Override // from CredentialManagerUiCallback + public abstract void onUiSelection(UserSelectionDialogResult selection); + + @Override // from CredentialManagerUiCallback + public abstract void onUiCancelation(); + + @Override // from ProviderInternalCallback + public abstract void onProviderStatusChanged(ProviderSession.Status status, ComponentName componentName); +} diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index b74fedf3ff46..593e64828ffc 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -154,6 +154,7 @@ import com.android.server.os.SchedulingPolicyService; import com.android.server.people.PeopleService; import com.android.server.pm.ApexManager; import com.android.server.pm.ApexSystemServiceInfo; +import com.android.server.pm.BackgroundInstallControlService; import com.android.server.pm.CrossProfileAppsService; import com.android.server.pm.DataLoaderManagerService; import com.android.server.pm.DynamicCodeLoggingService; @@ -2469,6 +2470,10 @@ public final class SystemServer implements Dumpable { t.traceBegin("StartMediaMetricsManager"); mSystemServiceManager.startService(MediaMetricsManagerService.class); t.traceEnd(); + + t.traceBegin("StartBackgroundInstallControlService"); + mSystemServiceManager.startService(BackgroundInstallControlService.class); + t.traceEnd(); } t.traceBegin("StartMediaProjectionManager"); diff --git a/services/print/java/com/android/server/print/PrintManagerService.java b/services/print/java/com/android/server/print/PrintManagerService.java index 66524edf61ed..35b9bc3b1e06 100644 --- a/services/print/java/com/android/server/print/PrintManagerService.java +++ b/services/print/java/com/android/server/print/PrintManagerService.java @@ -984,6 +984,7 @@ public final class PrintManagerService extends SystemService { monitor.register(mContext, BackgroundThread.getHandler().getLooper(), UserHandle.ALL, true); } + private UserState getOrCreateUserStateLocked(int userId, boolean lowPriority) { return getOrCreateUserStateLocked(userId, lowPriority, true /* enforceUserUnlockingOrUnlocked */); @@ -991,6 +992,12 @@ public final class PrintManagerService extends SystemService { private UserState getOrCreateUserStateLocked(int userId, boolean lowPriority, boolean enforceUserUnlockingOrUnlocked) { + return getOrCreateUserStateLocked(userId, lowPriority, + enforceUserUnlockingOrUnlocked, false /* shouldUpdateState */); + } + + private UserState getOrCreateUserStateLocked(int userId, boolean lowPriority, + boolean enforceUserUnlockingOrUnlocked, boolean shouldUpdateState) { if (enforceUserUnlockingOrUnlocked && !mUserManager.isUserUnlockingOrUnlocked(userId)) { throw new IllegalStateException( "User " + userId + " must be unlocked for printing to be available"); @@ -1000,6 +1007,8 @@ public final class PrintManagerService extends SystemService { if (userState == null) { userState = new UserState(mContext, userId, mLock, lowPriority); mUserStates.put(userId, userState); + } else if (shouldUpdateState) { + userState.updateIfNeededLocked(); } if (!lowPriority) { @@ -1019,9 +1028,9 @@ public final class PrintManagerService extends SystemService { UserState userState; synchronized (mLock) { - userState = getOrCreateUserStateLocked(userId, true, - false /*enforceUserUnlockingOrUnlocked */); - userState.updateIfNeededLocked(); + userState = getOrCreateUserStateLocked(userId, /* lowPriority */ true, + /* enforceUserUnlockingOrUnlocked */ false, + /* shouldUpdateState */ true); } // This is the first time we switch to this user after boot, so // now is the time to remove obsolete print jobs since they diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java index ba414cb593ef..5b7b8f4ca21f 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java @@ -33,6 +33,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import static org.mockito.Mockito.doReturn; import android.annotation.NonNull; @@ -43,6 +44,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.media.AudioManager; import android.os.Bundle; +import android.os.BundleMerger; import android.os.HandlerThread; import android.os.UserHandle; import android.provider.Settings; @@ -57,12 +59,15 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.junit.MockitoJUnitRunner; +import java.lang.reflect.Array; +import java.util.ArrayList; import java.util.List; @SmallTest @RunWith(MockitoJUnitRunner.class) public class BroadcastQueueModernImplTest { private static final int TEST_UID = android.os.Process.FIRST_APPLICATION_UID; + private static final int TEST_UID2 = android.os.Process.FIRST_APPLICATION_UID + 1; @Mock ActivityManagerService mAms; @Mock ProcessRecord mProcess; @@ -87,6 +92,10 @@ public class BroadcastQueueModernImplTest { mHandlerThread.start(); mConstants = new BroadcastConstants(Settings.Global.BROADCAST_FG_CONSTANTS); + mConstants.DELAY_URGENT_MILLIS = -120_000; + mConstants.DELAY_NORMAL_MILLIS = 10_000; + mConstants.DELAY_CACHED_MILLIS = 120_000; + mImpl = new BroadcastQueueModernImpl(mAms, mHandlerThread.getThreadHandler(), mConstants, mConstants); @@ -467,6 +476,62 @@ public class BroadcastQueueModernImplTest { List.of(musicVolumeChanged, alarmVolumeChanged, timeTick)); } + /** + * Verify that sending a broadcast with DELIVERY_GROUP_POLICY_MERGED works as expected. + */ + @Test + public void testDeliveryGroupPolicy_merged() { + final BundleMerger extrasMerger = new BundleMerger(); + extrasMerger.setMergeStrategy(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST, + BundleMerger.STRATEGY_ARRAY_APPEND); + + final Intent packageChangedForUid = createPackageChangedIntent(TEST_UID, + List.of("com.testuid.component1")); + final BroadcastOptions optionsPackageChangedForUid = BroadcastOptions.makeBasic(); + optionsPackageChangedForUid.setDeliveryGroupPolicy( + BroadcastOptions.DELIVERY_GROUP_POLICY_MERGED); + optionsPackageChangedForUid.setDeliveryGroupKey("package", String.valueOf(TEST_UID)); + optionsPackageChangedForUid.setDeliveryGroupExtrasMerger(extrasMerger); + + final Intent secondPackageChangedForUid = createPackageChangedIntent(TEST_UID, + List.of("com.testuid.component2", "com.testuid.component3")); + + final Intent packageChangedForUid2 = createPackageChangedIntent(TEST_UID2, + List.of("com.testuid2.component1")); + final BroadcastOptions optionsPackageChangedForUid2 = BroadcastOptions.makeBasic(); + optionsPackageChangedForUid.setDeliveryGroupPolicy( + BroadcastOptions.DELIVERY_GROUP_POLICY_MERGED); + optionsPackageChangedForUid.setDeliveryGroupKey("package", String.valueOf(TEST_UID2)); + optionsPackageChangedForUid.setDeliveryGroupExtrasMerger(extrasMerger); + + // Halt all processing so that we get a consistent view + mHandlerThread.getLooper().getQueue().postSyncBarrier(); + + mImpl.enqueueBroadcastLocked(makeBroadcastRecord(packageChangedForUid, + optionsPackageChangedForUid)); + mImpl.enqueueBroadcastLocked(makeBroadcastRecord(packageChangedForUid2, + optionsPackageChangedForUid2)); + mImpl.enqueueBroadcastLocked(makeBroadcastRecord(secondPackageChangedForUid, + optionsPackageChangedForUid)); + + final BroadcastProcessQueue queue = mImpl.getProcessQueue(PACKAGE_GREEN, + getUidForPackage(PACKAGE_GREEN)); + final Intent expectedPackageChangedForUid = createPackageChangedIntent(TEST_UID, + List.of("com.testuid.component2", "com.testuid.component3", + "com.testuid.component1")); + // Verify that packageChangedForUid and secondPackageChangedForUid broadcasts + // have been merged. + verifyPendingRecords(queue, List.of(packageChangedForUid2, expectedPackageChangedForUid)); + } + + private Intent createPackageChangedIntent(int uid, List<String> componentNameList) { + final Intent packageChangedIntent = new Intent(Intent.ACTION_PACKAGE_CHANGED); + packageChangedIntent.putExtra(Intent.EXTRA_UID, uid); + packageChangedIntent.putExtra(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST, + componentNameList.toArray()); + return packageChangedIntent; + } + private void verifyPendingRecords(BroadcastProcessQueue queue, List<Intent> intents) { for (int i = 0; i < intents.size(); i++) { @@ -477,9 +542,45 @@ public class BroadcastQueueModernImplTest { + ", actual_extras=" + actualIntent.getExtras() + ", expected_extras=" + expectedIntent.getExtras(); assertTrue(errMsg, actualIntent.filterEquals(expectedIntent)); - assertTrue(errMsg, Bundle.kindofEquals( - actualIntent.getExtras(), expectedIntent.getExtras())); + assertBundleEquals(expectedIntent.getExtras(), actualIntent.getExtras()); } assertTrue(queue.isEmpty()); } + + private void assertBundleEquals(Bundle expected, Bundle actual) { + final String errMsg = "expected=" + expected + ", actual=" + actual; + if (expected == actual) { + return; + } else if (expected == null || actual == null) { + fail(errMsg); + } + if (!expected.keySet().equals(actual.keySet())) { + fail(errMsg); + } + for (String key : expected.keySet()) { + final Object expectedValue = expected.get(key); + final Object actualValue = actual.get(key); + if (expectedValue == actualValue) { + continue; + } else if (expectedValue == null || actualValue == null) { + fail(errMsg); + } + assertEquals(errMsg, expectedValue.getClass(), actualValue.getClass()); + if (expectedValue.getClass().isArray()) { + assertEquals(errMsg, Array.getLength(expectedValue), Array.getLength(actualValue)); + for (int i = 0; i < Array.getLength(expectedValue); ++i) { + assertEquals(errMsg, Array.get(expectedValue, i), Array.get(actualValue, i)); + } + } else if (expectedValue instanceof ArrayList) { + final ArrayList<?> expectedList = (ArrayList<?>) expectedValue; + final ArrayList<?> actualList = (ArrayList<?>) actualValue; + assertEquals(errMsg, expectedList.size(), actualList.size()); + for (int i = 0; i < expectedList.size(); ++i) { + assertEquals(errMsg, expectedList.get(i), actualList.get(i)); + } + } else { + assertEquals(errMsg, expectedValue, actualValue); + } + } + } } diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java index d9a26c68f3ed..e1a4c1dd7256 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java @@ -888,7 +888,7 @@ public class BroadcastQueueTest { }) { // Confirm expected OOM adjustments; we were invoked once to upgrade // and once to downgrade - assertEquals(ActivityManager.PROCESS_STATE_RECEIVER, + assertEquals(String.valueOf(receiverApp), ActivityManager.PROCESS_STATE_RECEIVER, receiverApp.mState.getReportedProcState()); verify(mAms, times(2)).enqueueOomAdjTargetLocked(eq(receiverApp)); @@ -897,8 +897,8 @@ public class BroadcastQueueTest { // cold-started apps to be thawed, but the modern stack does } else { // Confirm that app was thawed - verify(mAms.mOomAdjuster.mCachedAppOptimizer).unfreezeTemporarily(eq(receiverApp), - eq(OomAdjuster.OOM_ADJ_REASON_START_RECEIVER)); + verify(mAms.mOomAdjuster.mCachedAppOptimizer, atLeastOnce()).unfreezeTemporarily( + eq(receiverApp), eq(OomAdjuster.OOM_ADJ_REASON_START_RECEIVER)); // Confirm that we added package to process verify(receiverApp, atLeastOnce()).addPackage(eq(receiverApp.info.packageName), @@ -1599,4 +1599,39 @@ public class BroadcastQueueTest { assertTrue(mQueue.isBeyondBarrierLocked(afterFirst)); assertTrue(mQueue.isBeyondBarrierLocked(afterSecond)); } + + /** + * Verify that we OOM adjust for manifest receivers. + */ + @Test + public void testOomAdjust_Manifest() throws Exception { + final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED); + + final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED); + enqueueBroadcast(makeBroadcastRecord(airplane, callerApp, + List.of(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN), + makeManifestReceiver(PACKAGE_GREEN, CLASS_BLUE), + makeManifestReceiver(PACKAGE_GREEN, CLASS_RED)))); + + waitForIdle(); + verify(mAms, atLeastOnce()).enqueueOomAdjTargetLocked(any()); + } + + /** + * Verify that we never OOM adjust for registered receivers. + */ + @Test + public void testOomAdjust_Registered() throws Exception { + final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED); + final ProcessRecord receiverApp = makeActiveProcessRecord(PACKAGE_GREEN); + + final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED); + enqueueBroadcast(makeBroadcastRecord(airplane, callerApp, + List.of(makeRegisteredReceiver(receiverApp), + makeRegisteredReceiver(receiverApp), + makeRegisteredReceiver(receiverApp)))); + + waitForIdle(); + verify(mAms, never()).enqueueOomAdjTargetLocked(any()); + } } diff --git a/services/tests/mockingservicestests/src/com/android/server/job/JobConcurrencyManagerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/JobConcurrencyManagerTest.java index f46877e5a8c6..25473477e8b9 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/JobConcurrencyManagerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/JobConcurrencyManagerTest.java @@ -16,10 +16,19 @@ package com.android.server.job; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.server.job.JobConcurrencyManager.KEY_PKG_CONCURRENCY_LIMIT_EJ; import static com.android.server.job.JobConcurrencyManager.KEY_PKG_CONCURRENCY_LIMIT_REGULAR; +import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_BG; +import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_BGUSER; +import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_BGUSER_IMPORTANT; +import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_EJ; +import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_FGS; +import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_NONE; +import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_TOP; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertFalse; @@ -47,15 +56,6 @@ import android.util.ArraySet; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; -import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_BG; -import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_BGUSER; -import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_BGUSER_IMPORTANT; -import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_EJ; -import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_FGS; -import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_NONE; -import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_TOP; - import com.android.internal.R; import com.android.internal.app.IBatteryStats; import com.android.server.LocalServices; @@ -73,6 +73,7 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoSession; import org.mockito.quality.Strictness; +import org.mockito.stubbing.Answer; import java.util.ArrayList; import java.util.List; @@ -124,6 +125,7 @@ public final class JobConcurrencyManagerTest { mMockingSession = mockitoSession() .initMocks(this) .mockStatic(AppGlobals.class) + .spyStatic(DeviceConfig.class) .strictness(Strictness.LENIENT) .startMocking(); final JobSchedulerService jobSchedulerService = mock(JobSchedulerService.class); @@ -134,6 +136,8 @@ public final class JobConcurrencyManagerTest { when(mContext.getResources()).thenReturn(mResources); doReturn(mContext).when(jobSchedulerService).getTestableContext(); mConfigBuilder = new DeviceConfig.Properties.Builder(DeviceConfig.NAMESPACE_JOB_SCHEDULER); + doAnswer((Answer<DeviceConfig.Properties>) invocationOnMock -> mConfigBuilder.build()) + .when(() -> DeviceConfig.getProperties(eq(DeviceConfig.NAMESPACE_JOB_SCHEDULER))); mPendingJobQueue = new PendingJobQueue(); doReturn(mPendingJobQueue).when(jobSchedulerService).getPendingJobQueue(); doReturn(mIPackageManager).when(AppGlobals::getPackageManager); @@ -595,7 +599,6 @@ public final class JobConcurrencyManagerTest { } private void updateDeviceConfig() throws Exception { - DeviceConfig.setProperties(mConfigBuilder.build()); mJobConcurrencyManager.updateConfigLocked(); } diff --git a/services/tests/mockingservicestests/src/com/android/server/job/restrictions/ThermalStatusRestrictionTest.java b/services/tests/mockingservicestests/src/com/android/server/job/restrictions/ThermalStatusRestrictionTest.java index aa959167b7f1..f88e18b4ced1 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/restrictions/ThermalStatusRestrictionTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/restrictions/ThermalStatusRestrictionTest.java @@ -184,8 +184,10 @@ public class ThermalStatusRestrictionTest { when(mJobSchedulerService.isCurrentlyRunningLocked(jobLowPriorityRunning)).thenReturn(true); when(mJobSchedulerService.isCurrentlyRunningLocked(jobHighPriorityRunning)) .thenReturn(true); - when(mJobSchedulerService.isLongRunningLocked(jobLowPriorityRunningLong)).thenReturn(true); - when(mJobSchedulerService.isLongRunningLocked(jobHighPriorityRunningLong)).thenReturn(true); + when(mJobSchedulerService.isJobInOvertimeLocked(jobLowPriorityRunningLong)) + .thenReturn(true); + when(mJobSchedulerService.isJobInOvertimeLocked(jobHighPriorityRunningLong)) + .thenReturn(true); assertFalse(mThermalStatusRestriction.isJobRestricted(jobMinPriority)); assertFalse(mThermalStatusRestriction.isJobRestricted(jobLowPriority)); diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundDexOptServiceUnitTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundDexOptServiceUnitTest.java index 47f449c7f897..1be7e2e6e30e 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundDexOptServiceUnitTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundDexOptServiceUnitTest.java @@ -223,7 +223,7 @@ public final class BackgroundDexOptServiceUnitTest { /* expectedReschedule= */ false, /* expectedStatus= */ STATUS_OK, /* totalJobFinishedWithParams= */ 1, /* expectedSkippedPackage= */ null); runFullJob(mJobServiceForIdle, mJobParametersForIdle, - /* expectedReschedule= */ true, /* expectedStatus= */ STATUS_OK, + /* expectedReschedule= */ false, /* expectedStatus= */ STATUS_OK, /* totalJobFinishedWithParams= */ 1, /* expectedSkippedPackage= */ null); } @@ -241,7 +241,7 @@ public final class BackgroundDexOptServiceUnitTest { assertThat(getFailedPackageNamesSecondary()).isEmpty(); runFullJob(mJobServiceForIdle, mJobParametersForIdle, - /* expectedReschedule= */ true, /* expectedStatus= */ STATUS_OK, + /* expectedReschedule= */ false, /* expectedStatus= */ STATUS_OK, /* totalJobFinishedWithParams= */ 1, /* expectedSkippedPackage= */ PACKAGE_AAA); assertThat(getFailedPackageNamesPrimary()).containsExactly(PACKAGE_AAA); @@ -256,7 +256,7 @@ public final class BackgroundDexOptServiceUnitTest { mDexOptResultForPackageAAA = PackageDexOptimizer.DEX_OPT_PERFORMED; runFullJob(mJobServiceForIdle, mJobParametersForIdle, - /* expectedReschedule= */ true, /* expectedStatus= */ STATUS_OK, + /* expectedReschedule= */ false, /* expectedStatus= */ STATUS_OK, /* totalJobFinishedWithParams= */ 2, /* expectedSkippedPackage= */ null); assertThat(getFailedPackageNamesPrimary()).isEmpty(); @@ -393,7 +393,7 @@ public final class BackgroundDexOptServiceUnitTest { mCancelThread.join(TEST_WAIT_TIMEOUT_MS); // Always reschedule for periodic job - verify(mJobServiceForIdle).jobFinished(mJobParametersForIdle, true); + verify(mJobServiceForIdle).jobFinished(mJobParametersForIdle, false); verifyLastControlDexOptBlockingCall(false); } @@ -421,7 +421,7 @@ public final class BackgroundDexOptServiceUnitTest { mCancelThread.join(TEST_WAIT_TIMEOUT_MS); // Always reschedule for periodic job - verify(mJobServiceForIdle).jobFinished(mJobParametersForIdle, true); + verify(mJobServiceForIdle).jobFinished(mJobParametersForIdle, false); verify(mDexOptHelper, never()).controlDexOptBlocking(true); } diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerInternalTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerInternalTest.java deleted file mode 100644 index 278e04ab1a41..000000000000 --- a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerInternalTest.java +++ /dev/null @@ -1,258 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.server.pm; - -import static android.os.UserHandle.USER_SYSTEM; -import static android.view.Display.DEFAULT_DISPLAY; -import static android.view.Display.INVALID_DISPLAY; - -import static com.google.common.truth.Truth.assertWithMessage; - -import static org.junit.Assert.assertThrows; - -import android.util.Log; - -import org.junit.Test; - -/** - * Run as {@code atest FrameworksMockingServicesTests:com.android.server.pm.UserManagerInternalTest} - */ -public final class UserManagerInternalTest extends UserManagerServiceOrInternalTestCase { - - private static final String TAG = UserManagerInternalTest.class.getSimpleName(); - - // NOTE: most the tests below only apply to MUMD configurations, so we're not adding _mumd_ - // in the test names, but _nonMumd_ instead - - @Test - public void testAssignUserToDisplay_nonMumd_defaultDisplayIgnored() { - mUmi.assignUserToDisplay(USER_ID, DEFAULT_DISPLAY); - - assertNoUserAssignedToDisplay(); - } - - @Test - public void testAssignUserToDisplay_nonMumd_otherDisplay_currentUser() { - mockCurrentUser(USER_ID); - - assertThrows(UnsupportedOperationException.class, - () -> mUmi.assignUserToDisplay(USER_ID, SECONDARY_DISPLAY_ID)); - } - - @Test - public void testAssignUserToDisplay_nonMumd_otherDisplay_startProfileOfcurrentUser() { - mockCurrentUser(PARENT_USER_ID); - addDefaultProfileAndParent(); - startDefaultProfile(); - - assertThrows(UnsupportedOperationException.class, - () -> mUmi.assignUserToDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID)); - } - - @Test - public void testAssignUserToDisplay_nonMumd_otherDisplay_stoppedProfileOfcurrentUser() { - mockCurrentUser(PARENT_USER_ID); - addDefaultProfileAndParent(); - stopDefaultProfile(); - - assertThrows(UnsupportedOperationException.class, - () -> mUmi.assignUserToDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID)); - } - - @Test - public void testAssignUserToDisplay_defaultDisplayIgnored() { - enableUsersOnSecondaryDisplays(); - - mUmi.assignUserToDisplay(USER_ID, DEFAULT_DISPLAY); - - assertNoUserAssignedToDisplay(); - } - - @Test - public void testAssignUserToDisplay_systemUser() { - enableUsersOnSecondaryDisplays(); - - assertThrows(IllegalArgumentException.class, - () -> mUmi.assignUserToDisplay(USER_SYSTEM, SECONDARY_DISPLAY_ID)); - } - - @Test - public void testAssignUserToDisplay_invalidDisplay() { - enableUsersOnSecondaryDisplays(); - - assertThrows(IllegalArgumentException.class, - () -> mUmi.assignUserToDisplay(USER_ID, INVALID_DISPLAY)); - } - - @Test - public void testAssignUserToDisplay_currentUser() { - enableUsersOnSecondaryDisplays(); - mockCurrentUser(USER_ID); - - assertThrows(IllegalArgumentException.class, - () -> mUmi.assignUserToDisplay(USER_ID, SECONDARY_DISPLAY_ID)); - - assertNoUserAssignedToDisplay(); - } - - @Test - public void testAssignUserToDisplay_startedProfileOfCurrentUser() { - enableUsersOnSecondaryDisplays(); - mockCurrentUser(PARENT_USER_ID); - addDefaultProfileAndParent(); - startDefaultProfile(); - - IllegalArgumentException e = assertThrows(IllegalArgumentException.class, - () -> mUmi.assignUserToDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID)); - - Log.v(TAG, "Exception: " + e); - assertNoUserAssignedToDisplay(); - } - - @Test - public void testAssignUserToDisplay_stoppedProfileOfCurrentUser() { - enableUsersOnSecondaryDisplays(); - mockCurrentUser(PARENT_USER_ID); - addDefaultProfileAndParent(); - stopDefaultProfile(); - - IllegalArgumentException e = assertThrows(IllegalArgumentException.class, - () -> mUmi.assignUserToDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID)); - - Log.v(TAG, "Exception: " + e); - assertNoUserAssignedToDisplay(); - } - - @Test - public void testAssignUserToDisplay_displayAvailable() { - enableUsersOnSecondaryDisplays(); - - mUmi.assignUserToDisplay(USER_ID, SECONDARY_DISPLAY_ID); - - assertUserAssignedToDisplay(USER_ID, SECONDARY_DISPLAY_ID); - } - - @Test - public void testAssignUserToDisplay_displayAlreadyAssigned() { - enableUsersOnSecondaryDisplays(); - - mUmi.assignUserToDisplay(USER_ID, SECONDARY_DISPLAY_ID); - - IllegalStateException e = assertThrows(IllegalStateException.class, - () -> mUmi.assignUserToDisplay(OTHER_USER_ID, SECONDARY_DISPLAY_ID)); - - Log.v(TAG, "Exception: " + e); - assertWithMessage("exception (%s) message", e).that(e).hasMessageThat() - .matches("Cannot.*" + OTHER_USER_ID + ".*" + SECONDARY_DISPLAY_ID + ".*already.*" - + USER_ID + ".*"); - } - - @Test - public void testAssignUserToDisplay_userAlreadyAssigned() { - enableUsersOnSecondaryDisplays(); - - mUmi.assignUserToDisplay(USER_ID, SECONDARY_DISPLAY_ID); - - IllegalStateException e = assertThrows(IllegalStateException.class, - () -> mUmi.assignUserToDisplay(USER_ID, OTHER_SECONDARY_DISPLAY_ID)); - - Log.v(TAG, "Exception: " + e); - assertWithMessage("exception (%s) message", e).that(e).hasMessageThat() - .matches("Cannot.*" + USER_ID + ".*" + OTHER_SECONDARY_DISPLAY_ID + ".*already.*" - + SECONDARY_DISPLAY_ID + ".*"); - - assertUserAssignedToDisplay(USER_ID, SECONDARY_DISPLAY_ID); - } - - @Test - public void testAssignUserToDisplay_profileOnSameDisplayAsParent() { - enableUsersOnSecondaryDisplays(); - addDefaultProfileAndParent(); - - mUmi.assignUserToDisplay(PARENT_USER_ID, SECONDARY_DISPLAY_ID); - IllegalArgumentException e = assertThrows(IllegalArgumentException.class, - () -> mUmi.assignUserToDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID)); - - Log.v(TAG, "Exception: " + e); - assertUserAssignedToDisplay(PARENT_USER_ID, SECONDARY_DISPLAY_ID); - } - - @Test - public void testAssignUserToDisplay_profileOnDifferentDisplayAsParent() { - enableUsersOnSecondaryDisplays(); - addDefaultProfileAndParent(); - - mUmi.assignUserToDisplay(PARENT_USER_ID, SECONDARY_DISPLAY_ID); - IllegalArgumentException e = assertThrows(IllegalArgumentException.class, - () -> mUmi.assignUserToDisplay(PROFILE_USER_ID, OTHER_SECONDARY_DISPLAY_ID)); - - Log.v(TAG, "Exception: " + e); - assertUserAssignedToDisplay(PARENT_USER_ID, SECONDARY_DISPLAY_ID); - } - - @Test - public void testAssignUserToDisplay_profileDefaultDisplayParentOnSecondaryDisplay() { - enableUsersOnSecondaryDisplays(); - addDefaultProfileAndParent(); - - mUmi.assignUserToDisplay(PARENT_USER_ID, SECONDARY_DISPLAY_ID); - IllegalArgumentException e = assertThrows(IllegalArgumentException.class, - () -> mUmi.assignUserToDisplay(PROFILE_USER_ID, DEFAULT_DISPLAY)); - - Log.v(TAG, "Exception: " + e); - assertUserAssignedToDisplay(PARENT_USER_ID, SECONDARY_DISPLAY_ID); - } - - @Test - public void testUnassignUserFromDisplay_nonMumd_ignored() { - mockCurrentUser(USER_ID); - - mUmi.unassignUserFromDisplay(USER_SYSTEM); - mUmi.unassignUserFromDisplay(USER_ID); - mUmi.unassignUserFromDisplay(OTHER_USER_ID); - - assertNoUserAssignedToDisplay(); - } - - @Test - public void testUnassignUserFromDisplay() { - testAssignUserToDisplay_displayAvailable(); - - mUmi.unassignUserFromDisplay(USER_ID); - - assertNoUserAssignedToDisplay(); - } - - @Override - protected boolean isUserVisible(int userId) { - return mUmi.isUserVisible(userId); - } - - @Override - protected boolean isUserVisibleOnDisplay(int userId, int displayId) { - return mUmi.isUserVisible(userId, displayId); - } - - @Override - protected int getDisplayAssignedToUser(int userId) { - return mUmi.getDisplayAssignedToUser(userId); - } - - @Override - protected int getUserAssignedToDisplay(int displayId) { - return mUmi.getUserAssignedToDisplay(displayId); - } -} diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceOrInternalTestCase.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceOrInternalTestCase.java index 90a5fa0d9f38..6c85b7ab4985 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceOrInternalTestCase.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceOrInternalTestCase.java @@ -15,10 +15,6 @@ */ package com.android.server.pm; -import static android.os.UserHandle.USER_NULL; -import static android.view.Display.DEFAULT_DISPLAY; -import static android.view.Display.INVALID_DISPLAY; - import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; @@ -34,7 +30,6 @@ import android.content.pm.UserInfo; import android.os.UserManager; import android.util.Log; import android.util.SparseArray; -import android.util.SparseIntArray; import androidx.test.annotation.UiThreadTest; @@ -46,12 +41,8 @@ import com.android.server.pm.UserManagerService.UserData; import org.junit.After; import org.junit.Before; -import org.junit.Test; import org.mockito.Mock; -import java.util.LinkedHashMap; -import java.util.Map; - /** * Base class for {@link UserManagerInternalTest} and {@link UserManagerInternalTest}. * @@ -59,9 +50,13 @@ import java.util.Map; * "symbiotic relationship - some methods of the former simply call the latter and vice versa. * * <p>Ideally, only one of them should have the logic, but since that's not the case, this class - * provices the infra to make it easier to test both (which in turn would make it easier / safer to + * provides the infra to make it easier to test both (which in turn would make it easier / safer to * refactor their logic later). */ +// TODO(b/244644281): there is no UserManagerInternalTest anymore as the logic being tested there +// moved to UserVisibilityController, so it might be simpler to merge this class into +// UserManagerServiceTest (once the UserVisibilityController -> UserManagerService dependency is +// fixed) abstract class UserManagerServiceOrInternalTestCase extends ExtendedMockitoTestCase { private static final String TAG = UserManagerServiceOrInternalTestCase.class.getSimpleName(); @@ -90,31 +85,12 @@ abstract class UserManagerServiceOrInternalTestCase extends ExtendedMockitoTestC */ protected static final int PROFILE_USER_ID = 643; - /** - * Id of a secondary display (i.e, not {@link android.view.Display.DEFAULT_DISPLAY}). - */ - protected static final int SECONDARY_DISPLAY_ID = 42; - - /** - * Id of another secondary display (i.e, not {@link android.view.Display.DEFAULT_DISPLAY}). - */ - protected static final int OTHER_SECONDARY_DISPLAY_ID = 108; - private final Object mPackagesLock = new Object(); private final Context mRealContext = androidx.test.InstrumentationRegistry.getInstrumentation() .getTargetContext(); private final SparseArray<UserData> mUsers = new SparseArray<>(); - // TODO(b/244644281): manipulating mUsersOnSecondaryDisplays directly leaks implementation - // details into the unit test, but it's fine for now - in the long term, this logic should be - // refactored into a proper UserDisplayAssignment class. - private final SparseIntArray mUsersOnSecondaryDisplays = new SparseIntArray(); - private Context mSpiedContext; - private UserManagerService mStandardUms; - private UserManagerService mMumdUms; - private UserManagerInternal mStandardUmi; - private UserManagerInternal mMumdUmi; private @Mock PackageManagerService mMockPms; private @Mock UserDataPreparer mMockUserDataPreparer; @@ -122,17 +98,11 @@ abstract class UserManagerServiceOrInternalTestCase extends ExtendedMockitoTestC /** * Reference to the {@link UserManagerService} being tested. - * - * <p>By default, such service doesn't support {@code MUMD} (Multiple Users on Multiple - * Displays), but that can be changed by calling {@link #enableUsersOnSecondaryDisplays()}. */ protected UserManagerService mUms; /** * Reference to the {@link UserManagerInternal} being tested. - * - * <p>By default, such service doesn't support {@code MUMD} (Multiple Users on Multiple - * Displays), but that can be changed by calling {@link #enableUsersOnSecondaryDisplays()}. */ protected UserManagerInternal mUmi; @@ -151,32 +121,12 @@ abstract class UserManagerServiceOrInternalTestCase extends ExtendedMockitoTestC // Called when WatchedUserStates is constructed doNothing().when(() -> UserManager.invalidateIsUserUnlockedCache()); - // Need to set both UserManagerService instances here, as they need to be run in the - // UiThread - - // mMumdUms / mMumdUmi - mockIsUsersOnSecondaryDisplaysEnabled(/* usersOnSecondaryDisplaysEnabled= */ true); - mMumdUms = new UserManagerService(mSpiedContext, mMockPms, mMockUserDataPreparer, - mPackagesLock, mRealContext.getDataDir(), mUsers, mUsersOnSecondaryDisplays); - assertWithMessage("UserManagerService.isUsersOnSecondaryDisplaysEnabled()") - .that(mMumdUms.isUsersOnSecondaryDisplaysEnabled()) - .isTrue(); - mMumdUmi = LocalServices.getService(UserManagerInternal.class); - assertWithMessage("LocalServices.getService(UserManagerInternal.class)").that(mMumdUmi) - .isNotNull(); - resetUserManagerInternal(); - - // mStandardUms / mStandardUmi - mockIsUsersOnSecondaryDisplaysEnabled(/* usersOnSecondaryDisplaysEnabled= */ false); - mStandardUms = new UserManagerService(mSpiedContext, mMockPms, mMockUserDataPreparer, - mPackagesLock, mRealContext.getDataDir(), mUsers, mUsersOnSecondaryDisplays); - assertWithMessage("UserManagerService.isUsersOnSecondaryDisplaysEnabled()") - .that(mStandardUms.isUsersOnSecondaryDisplaysEnabled()) - .isFalse(); - mStandardUmi = LocalServices.getService(UserManagerInternal.class); - assertWithMessage("LocalServices.getService(UserManagerInternal.class)").that(mStandardUmi) + // Must construct UserManagerService in the UiThread + mUms = new UserManagerService(mSpiedContext, mMockPms, mMockUserDataPreparer, + mPackagesLock, mRealContext.getDataDir(), mUsers); + mUmi = LocalServices.getService(UserManagerInternal.class); + assertWithMessage("LocalServices.getService(UserManagerInternal.class)").that(mUmi) .isNotNull(); - setServiceFixtures(/*usersOnSecondaryDisplaysEnabled= */ false); } @After @@ -185,373 +135,10 @@ abstract class UserManagerServiceOrInternalTestCase extends ExtendedMockitoTestC LocalServices.removeServiceForTest(UserManagerInternal.class); } - ////////////////////////////////////////////////////////////////////////////////////////////// - // Methods whose UMS implementation calls UMI or vice-versa - they're tested in this class, // - // but the subclass must provide the proper implementation // - ////////////////////////////////////////////////////////////////////////////////////////////// - - protected abstract boolean isUserVisible(int userId); - protected abstract boolean isUserVisibleOnDisplay(int userId, int displayId); - protected abstract int getDisplayAssignedToUser(int userId); - protected abstract int getUserAssignedToDisplay(int displayId); - - ///////////////////////////////// - // Tests for the above methods // - ///////////////////////////////// - - @Test - public void testIsUserVisible_invalidUser() { - mockCurrentUser(USER_ID); - - assertWithMessage("isUserVisible(%s)", USER_NULL).that(isUserVisible(USER_NULL)).isFalse(); - } - - @Test - public void testIsUserVisible_currentUser() { - mockCurrentUser(USER_ID); - - assertWithMessage("isUserVisible(%s)", USER_ID).that(isUserVisible(USER_ID)).isTrue(); - } - - @Test - public void testIsUserVisible_nonCurrentUser() { - mockCurrentUser(OTHER_USER_ID); - - assertWithMessage("isUserVisible(%s)", USER_ID).that(isUserVisible(USER_ID)).isFalse(); - } - - @Test - public void testIsUserVisible_startedProfileOfcurrentUser() { - addDefaultProfileAndParent(); - mockCurrentUser(PARENT_USER_ID); - startDefaultProfile(); - setUserState(PROFILE_USER_ID, UserState.STATE_RUNNING_UNLOCKED); - - assertWithMessage("isUserVisible(%s)", PROFILE_USER_ID).that(isUserVisible(PROFILE_USER_ID)) - .isTrue(); - } - - @Test - public void testIsUserVisible_stoppedProfileOfcurrentUser() { - addDefaultProfileAndParent(); - mockCurrentUser(PARENT_USER_ID); - stopDefaultProfile(); - - assertWithMessage("isUserVisible(%s)", PROFILE_USER_ID).that(isUserVisible(PROFILE_USER_ID)) - .isFalse(); - } - - @Test - public void testIsUserVisible_bgUserOnSecondaryDisplay() { - enableUsersOnSecondaryDisplays(); - mockCurrentUser(OTHER_USER_ID); - assignUserToDisplay(USER_ID, SECONDARY_DISPLAY_ID); - - assertWithMessage("isUserVisible(%s)", USER_ID).that(isUserVisible(USER_ID)).isTrue(); - } - - // NOTE: we don't need to add tests for profiles (started / stopped profiles of bg user), as - // isUserVisible() for bg users relies only on the user / display assignments - - @Test - public void testIsUserVisibleOnDisplay_invalidUser() { - mockCurrentUser(USER_ID); - - assertWithMessage("isUserVisibleOnDisplay(%s, %s)", USER_NULL, DEFAULT_DISPLAY) - .that(isUserVisibleOnDisplay(USER_NULL, DEFAULT_DISPLAY)).isFalse(); - } - - @Test - public void testIsUserVisibleOnDisplay_currentUserInvalidDisplay() { - mockCurrentUser(USER_ID); - - assertWithMessage("isUserVisibleOnDisplay(%s, %s)", USER_ID, INVALID_DISPLAY) - .that(isUserVisibleOnDisplay(USER_ID, INVALID_DISPLAY)).isFalse(); - } - - @Test - public void testIsUserVisibleOnDisplay_currentUserDefaultDisplay() { - mockCurrentUser(USER_ID); - - assertWithMessage("isUserVisibleOnDisplay(%s, %s)", USER_ID, DEFAULT_DISPLAY) - .that(isUserVisibleOnDisplay(USER_ID, DEFAULT_DISPLAY)).isTrue(); - } - - @Test - public void testIsUserVisibleOnDisplay_currentUserSecondaryDisplay() { - mockCurrentUser(USER_ID); - - assertWithMessage("isUserVisibleOnDisplay(%s, %s)", USER_ID, SECONDARY_DISPLAY_ID) - .that(isUserVisibleOnDisplay(USER_ID, SECONDARY_DISPLAY_ID)).isTrue(); - } - - @Test - public void testIsUserVisibleOnDisplay_mumd_currentUserUnassignedSecondaryDisplay() { - enableUsersOnSecondaryDisplays(); - mockCurrentUser(USER_ID); - - assertWithMessage("isUserVisibleOnDisplay(%s, %s)", USER_ID, SECONDARY_DISPLAY_ID) - .that(isUserVisibleOnDisplay(USER_ID, SECONDARY_DISPLAY_ID)).isTrue(); - } - - @Test - public void testIsUserVisibleOnDisplay_mumd_currentUserSecondaryDisplayAssignedToAnotherUser() { - enableUsersOnSecondaryDisplays(); - mockCurrentUser(USER_ID); - assignUserToDisplay(OTHER_USER_ID, SECONDARY_DISPLAY_ID); - - assertWithMessage("isUserVisibleOnDisplay(%s, %s)", USER_ID, SECONDARY_DISPLAY_ID) - .that(isUserVisibleOnDisplay(USER_ID, SECONDARY_DISPLAY_ID)).isFalse(); - } - - @Test - public void testIsUserVisibleOnDisplay_mumd_startedProfileOfCurrentUserSecondaryDisplayAssignedToAnotherUser() { - enableUsersOnSecondaryDisplays(); - addDefaultProfileAndParent(); - startDefaultProfile(); - mockCurrentUser(PARENT_USER_ID); - assignUserToDisplay(OTHER_USER_ID, SECONDARY_DISPLAY_ID); - - assertWithMessage("isUserVisibleOnDisplay(%s, %s)", PROFILE_USER_ID, SECONDARY_DISPLAY_ID) - .that(isUserVisibleOnDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID)).isFalse(); - } - - @Test - public void testIsUserVisibleOnDisplay_mumd_stoppedProfileOfCurrentUserSecondaryDisplayAssignedToAnotherUser() { - enableUsersOnSecondaryDisplays(); - addDefaultProfileAndParent(); - stopDefaultProfile(); - mockCurrentUser(PARENT_USER_ID); - assignUserToDisplay(OTHER_USER_ID, SECONDARY_DISPLAY_ID); - - assertWithMessage("isUserVisibleOnDisplay(%s, %s)", PROFILE_USER_ID, SECONDARY_DISPLAY_ID) - .that(isUserVisibleOnDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID)).isFalse(); - } - - @Test - public void testIsUserVisibleOnDisplay_nonCurrentUserDefaultDisplay() { - mockCurrentUser(OTHER_USER_ID); - - assertWithMessage("isUserVisibleOnDisplay(%s, %s)", USER_ID, DEFAULT_DISPLAY) - .that(isUserVisibleOnDisplay(USER_ID, DEFAULT_DISPLAY)).isFalse(); - } - - @Test - public void testIsUserVisibleOnDisplay_startedProfileOfcurrentUserInvalidDisplay() { - addDefaultProfileAndParent(); - mockCurrentUser(PARENT_USER_ID); - startDefaultProfile(); - - assertWithMessage("isUserVisibleOnDisplay(%s, %s)", PROFILE_USER_ID, INVALID_DISPLAY) - .that(isUserVisibleOnDisplay(PROFILE_USER_ID, DEFAULT_DISPLAY)).isTrue(); - } - - @Test - public void testIsUserVisibleOnDisplay_stoppedProfileOfcurrentUserInvalidDisplay() { - addDefaultProfileAndParent(); - mockCurrentUser(PARENT_USER_ID); - stopDefaultProfile(); - - assertWithMessage("isUserVisibleOnDisplay(%s, %s)", PROFILE_USER_ID, INVALID_DISPLAY) - .that(isUserVisibleOnDisplay(PROFILE_USER_ID, DEFAULT_DISPLAY)).isFalse(); - } - - @Test - public void testIsUserVisibleOnDisplay_startedProfileOfcurrentUserDefaultDisplay() { - addDefaultProfileAndParent(); - mockCurrentUser(PARENT_USER_ID); - startDefaultProfile(); - setUserState(PROFILE_USER_ID, UserState.STATE_RUNNING_UNLOCKED); - - assertWithMessage("isUserVisibleOnDisplay(%s, %s)", PROFILE_USER_ID, DEFAULT_DISPLAY) - .that(isUserVisibleOnDisplay(PROFILE_USER_ID, DEFAULT_DISPLAY)).isTrue(); - } - - @Test - public void testIsUserVisibleOnDisplay_stoppedProfileOfcurrentUserDefaultDisplay() { - addDefaultProfileAndParent(); - mockCurrentUser(PARENT_USER_ID); - stopDefaultProfile(); - - assertWithMessage("isUserVisibleOnDisplay(%s, %s)", PROFILE_USER_ID, DEFAULT_DISPLAY) - .that(isUserVisibleOnDisplay(PROFILE_USER_ID, DEFAULT_DISPLAY)).isFalse(); - } - - @Test - public void testIsUserVisibleOnDisplay_startedProfileOfcurrentUserSecondaryDisplay() { - addDefaultProfileAndParent(); - mockCurrentUser(PARENT_USER_ID); - startDefaultProfile(); - setUserState(PROFILE_USER_ID, UserState.STATE_RUNNING_UNLOCKED); - - assertWithMessage("isUserVisibleOnDisplay(%s, %s)", PROFILE_USER_ID, SECONDARY_DISPLAY_ID) - .that(isUserVisibleOnDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID)).isTrue(); - } - - @Test - public void testIsUserVisibleOnDisplay_stoppedProfileOfcurrentUserSecondaryDisplay() { - addDefaultProfileAndParent(); - mockCurrentUser(PARENT_USER_ID); - stopDefaultProfile(); - - assertWithMessage("isUserVisibleOnDisplay(%s, %s)", PROFILE_USER_ID, SECONDARY_DISPLAY_ID) - .that(isUserVisibleOnDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID)).isFalse(); - } - - @Test - public void testIsUserVisibleOnDisplay_mumd_bgUserOnSecondaryDisplay() { - enableUsersOnSecondaryDisplays(); - mockCurrentUser(OTHER_USER_ID); - assignUserToDisplay(USER_ID, SECONDARY_DISPLAY_ID); - - assertWithMessage("isUserVisibleOnDisplay(%s, %s)", USER_ID, SECONDARY_DISPLAY_ID) - .that(isUserVisibleOnDisplay(USER_ID, SECONDARY_DISPLAY_ID)).isTrue(); - } - - @Test - public void testIsUserVisibleOnDisplay_mumd_bgUserOnAnotherSecondaryDisplay() { - enableUsersOnSecondaryDisplays(); - mockCurrentUser(OTHER_USER_ID); - assignUserToDisplay(USER_ID, SECONDARY_DISPLAY_ID); - - assertWithMessage("isUserVisibleOnDisplay(%s, %s)", USER_ID, SECONDARY_DISPLAY_ID) - .that(isUserVisibleOnDisplay(USER_ID, OTHER_SECONDARY_DISPLAY_ID)).isFalse(); - } - - // NOTE: we don't need to add tests for profiles (started / stopped profiles of bg user), as - // isUserVisibleOnDisplay() for bg users relies only on the user / display assignments - - @Test - public void testGetDisplayAssignedToUser_invalidUser() { - mockCurrentUser(USER_ID); - - assertWithMessage("getDisplayAssignedToUser(%s)", USER_NULL) - .that(getDisplayAssignedToUser(USER_NULL)).isEqualTo(INVALID_DISPLAY); - } - - @Test - public void testGetDisplayAssignedToUser_currentUser() { - mockCurrentUser(USER_ID); - - assertWithMessage("getDisplayAssignedToUser(%s)", USER_ID) - .that(getDisplayAssignedToUser(USER_ID)).isEqualTo(DEFAULT_DISPLAY); - } - - @Test - public void testGetDisplayAssignedToUser_nonCurrentUser() { - mockCurrentUser(OTHER_USER_ID); - - assertWithMessage("getDisplayAssignedToUser(%s)", USER_ID) - .that(getDisplayAssignedToUser(USER_ID)).isEqualTo(INVALID_DISPLAY); - } - - @Test - public void testGetDisplayAssignedToUser_startedProfileOfcurrentUser() { - addDefaultProfileAndParent(); - mockCurrentUser(PARENT_USER_ID); - startDefaultProfile(); - setUserState(PROFILE_USER_ID, UserState.STATE_RUNNING_UNLOCKED); - - assertWithMessage("getDisplayAssignedToUser(%s)", PROFILE_USER_ID) - .that(getDisplayAssignedToUser(PROFILE_USER_ID)).isEqualTo(DEFAULT_DISPLAY); - } - - @Test - public void testGetDisplayAssignedToUser_stoppedProfileOfcurrentUser() { - addDefaultProfileAndParent(); - mockCurrentUser(PARENT_USER_ID); - stopDefaultProfile(); - - assertWithMessage("getDisplayAssignedToUser(%s)", PROFILE_USER_ID) - .that(getDisplayAssignedToUser(PROFILE_USER_ID)).isEqualTo(INVALID_DISPLAY); - } - - @Test - public void testGetDisplayAssignedToUser_bgUserOnSecondaryDisplay() { - enableUsersOnSecondaryDisplays(); - mockCurrentUser(OTHER_USER_ID); - assignUserToDisplay(USER_ID, SECONDARY_DISPLAY_ID); - - assertWithMessage("getDisplayAssignedToUser(%s)", USER_ID) - .that(getDisplayAssignedToUser(USER_ID)).isEqualTo(SECONDARY_DISPLAY_ID); - } - - // NOTE: we don't need to add tests for profiles (started / stopped profiles of bg user), as - // getDisplayAssignedToUser() for bg users relies only on the user / display assignments - - @Test - public void testGetUserAssignedToDisplay_invalidDisplay() { - mockCurrentUser(USER_ID); - - assertWithMessage("getUserAssignedToDisplay(%s)", INVALID_DISPLAY) - .that(getUserAssignedToDisplay(INVALID_DISPLAY)).isEqualTo(USER_ID); - } - - @Test - public void testGetUserAssignedToDisplay_defaultDisplay() { - mockCurrentUser(USER_ID); - - assertWithMessage("getUserAssignedToDisplay(%s)", DEFAULT_DISPLAY) - .that(getUserAssignedToDisplay(DEFAULT_DISPLAY)).isEqualTo(USER_ID); - } - - @Test - public void testGetUserAssignedToDisplay_secondaryDisplay() { - mockCurrentUser(USER_ID); - - assertWithMessage("getUserAssignedToDisplay(%s)", SECONDARY_DISPLAY_ID) - .that(getUserAssignedToDisplay(SECONDARY_DISPLAY_ID)).isEqualTo(USER_ID); - } - - @Test - public void testGetUserAssignedToDisplay_mumd_bgUserOnSecondaryDisplay() { - enableUsersOnSecondaryDisplays(); - mockCurrentUser(OTHER_USER_ID); - assignUserToDisplay(USER_ID, SECONDARY_DISPLAY_ID); - - assertWithMessage("getUserAssignedToDisplay(%s)", SECONDARY_DISPLAY_ID) - .that(getUserAssignedToDisplay(SECONDARY_DISPLAY_ID)).isEqualTo(USER_ID); - } - - @Test - public void testGetUserAssignedToDisplay_mumd_noUserOnSecondaryDisplay() { - enableUsersOnSecondaryDisplays(); - mockCurrentUser(USER_ID); - - assertWithMessage("getUserAssignedToDisplay(%s)", SECONDARY_DISPLAY_ID) - .that(getUserAssignedToDisplay(SECONDARY_DISPLAY_ID)).isEqualTo(USER_ID); - } - - // TODO(b/244644281): scenario below shouldn't happen on "real life", as the profile cannot be - // started on secondary display if its parent isn't, so we might need to remove (or refactor - // this test) if/when the underlying logic changes - @Test - public void testGetUserAssignedToDisplay_mumd_profileOnSecondaryDisplay() { - enableUsersOnSecondaryDisplays(); - addDefaultProfileAndParent(); - mockCurrentUser(USER_ID); - assignUserToDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID); - - assertWithMessage("getUserAssignedToDisplay(%s)", SECONDARY_DISPLAY_ID) - .that(getUserAssignedToDisplay(SECONDARY_DISPLAY_ID)).isEqualTo(USER_ID); - } - - // NOTE: we don't need to add tests for profiles (started / stopped profiles of bg user), as - // getUserAssignedToDisplay() for bg users relies only on the user / display assignments - - /////////////////////////////////////////// // Helper methods exposed to sub-classes // /////////////////////////////////////////// - /** - * Change test fixtures to use a version that supports {@code MUMD} (Multiple Users on Multiple - * Displays). - */ - protected final void enableUsersOnSecondaryDisplays() { - setServiceFixtures(/* usersOnSecondaryDisplaysEnabled= */ true); - } - protected final void mockCurrentUser(@UserIdInt int userId) { mockGetLocalService(ActivityManagerInternal.class, mActivityManagerInternal); @@ -597,65 +184,19 @@ abstract class UserManagerServiceOrInternalTestCase extends ExtendedMockitoTestC setUserState(userId, UserState.STATE_STOPPING); } - // NOTE: should only called by tests that indirectly needs to check user assignments (like - // isUserVisible), not by tests for the user assignment methods per se. - protected final void assignUserToDisplay(@UserIdInt int userId, int displayId) { - mUsersOnSecondaryDisplays.put(userId, displayId); - } - - protected final void assertNoUserAssignedToDisplay() { - assertWithMessage("mUsersOnSecondaryDisplays()").that(usersOnSecondaryDisplaysAsMap()) - .isEmpty(); - } - - protected final void assertUserAssignedToDisplay(@UserIdInt int userId, int displayId) { - assertWithMessage("mUsersOnSecondaryDisplays()").that(usersOnSecondaryDisplaysAsMap()) - .containsExactly(userId, displayId); + protected final void setUserState(@UserIdInt int userId, int userState) { + mUmi.setUserState(userId, userState); } /////////////////// // Private infra // /////////////////// - private void setServiceFixtures(boolean usersOnSecondaryDisplaysEnabled) { - Log.d(TAG, "Setting fixtures for usersOnSecondaryDisplaysEnabled=" - + usersOnSecondaryDisplaysEnabled); - if (usersOnSecondaryDisplaysEnabled) { - mUms = mMumdUms; - mUmi = mMumdUmi; - } else { - mUms = mStandardUms; - mUmi = mStandardUmi; - } - } - - private void mockIsUsersOnSecondaryDisplaysEnabled(boolean enabled) { - Log.d(TAG, "Mocking UserManager.isUsersOnSecondaryDisplaysEnabled() to return " + enabled); - doReturn(enabled).when(() -> UserManager.isUsersOnSecondaryDisplaysEnabled()); - } - private void addUserData(TestUserData userData) { Log.d(TAG, "Adding " + userData); mUsers.put(userData.info.id, userData); } - private void setUserState(@UserIdInt int userId, int userState) { - mUmi.setUserState(userId, userState); - } - - private void removeUserState(@UserIdInt int userId) { - mUmi.removeUserState(userId); - } - - private Map<Integer, Integer> usersOnSecondaryDisplaysAsMap() { - int size = mUsersOnSecondaryDisplays.size(); - Map<Integer, Integer> map = new LinkedHashMap<>(size); - for (int i = 0; i < size; i++) { - map.put(mUsersOnSecondaryDisplays.keyAt(i), mUsersOnSecondaryDisplays.valueAt(i)); - } - return map; - } - private static final class TestUserData extends UserData { @SuppressWarnings("deprecation") diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java index 8b5921cd45bd..b5ffe5f24338 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java @@ -123,24 +123,4 @@ public final class UserManagerServiceTest extends UserManagerServiceOrInternalTe assertWithMessage("isUserRunning(%s)", PROFILE_USER_ID) .that(mUms.isUserRunning(PROFILE_USER_ID)).isFalse(); } - - @Override - protected boolean isUserVisible(int userId) { - return mUms.isUserVisibleUnchecked(userId); - } - - @Override - protected boolean isUserVisibleOnDisplay(int userId, int displayId) { - return mUms.isUserVisibleOnDisplay(userId, displayId); - } - - @Override - protected int getDisplayAssignedToUser(int userId) { - return mUms.getDisplayAssignedToUser(userId); - } - - @Override - protected int getUserAssignedToDisplay(int displayId) { - return mUms.getUserAssignedToDisplay(displayId); - } } diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorMUMDTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorMUMDTest.java new file mode 100644 index 000000000000..21f541f54790 --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorMUMDTest.java @@ -0,0 +1,303 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.pm; + +import static android.os.UserHandle.USER_SYSTEM; +import static android.view.Display.DEFAULT_DISPLAY; +import static android.view.Display.INVALID_DISPLAY; + +import static com.google.common.truth.Truth.assertWithMessage; + +import static org.junit.Assert.assertThrows; + +import android.util.Log; + +import org.junit.Test; + +/** + * Tests for {@link UserVisibilityMediator} tests for devices that support concurrent Multiple + * Users on Multiple Displays (A.K.A {@code MUMD}). + * + * <p>Run as + * {@code atest FrameworksMockingServicesTests:com.android.server.pm.UserVisibilityMediatorMUMDTest} + */ +public final class UserVisibilityMediatorMUMDTest extends UserVisibilityMediatorTestCase { + + private static final String TAG = UserVisibilityMediatorMUMDTest.class.getSimpleName(); + + public UserVisibilityMediatorMUMDTest() { + super(/* usersOnSecondaryDisplaysEnabled= */ true); + } + + @Test + public void testAssignUserToDisplay_systemUser() { + assertThrows(IllegalArgumentException.class, + () -> mMediator.assignUserToDisplay(USER_SYSTEM, SECONDARY_DISPLAY_ID)); + } + + @Test + public void testAssignUserToDisplay_invalidDisplay() { + assertThrows(IllegalArgumentException.class, + () -> mMediator.assignUserToDisplay(USER_ID, INVALID_DISPLAY)); + } + + @Test + public void testAssignUserToDisplay_currentUser() { + mockCurrentUser(USER_ID); + + assertThrows(IllegalArgumentException.class, + () -> mMediator.assignUserToDisplay(USER_ID, SECONDARY_DISPLAY_ID)); + + assertNoUserAssignedToDisplay(); + } + + @Test + public void testAssignUserToDisplay_startedProfileOfCurrentUser() { + mockCurrentUser(PARENT_USER_ID); + addDefaultProfileAndParent(); + startDefaultProfile(); + + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> mMediator.assignUserToDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID)); + + Log.v(TAG, "Exception: " + e); + assertNoUserAssignedToDisplay(); + } + + @Test + public void testAssignUserToDisplay_stoppedProfileOfCurrentUser() { + mockCurrentUser(PARENT_USER_ID); + addDefaultProfileAndParent(); + stopDefaultProfile(); + + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> mMediator.assignUserToDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID)); + + Log.v(TAG, "Exception: " + e); + assertNoUserAssignedToDisplay(); + } + + @Test + public void testAssignUserToDisplay_displayAvailable() { + mMediator.assignUserToDisplay(USER_ID, SECONDARY_DISPLAY_ID); + + assertUserAssignedToDisplay(USER_ID, SECONDARY_DISPLAY_ID); + } + + @Test + public void testAssignUserToDisplay_displayAlreadyAssigned() { + mMediator.assignUserToDisplay(USER_ID, SECONDARY_DISPLAY_ID); + + IllegalStateException e = assertThrows(IllegalStateException.class, + () -> mMediator.assignUserToDisplay(OTHER_USER_ID, SECONDARY_DISPLAY_ID)); + + Log.v(TAG, "Exception: " + e); + assertWithMessage("exception (%s) message", e).that(e).hasMessageThat() + .matches("Cannot.*" + OTHER_USER_ID + ".*" + SECONDARY_DISPLAY_ID + ".*already.*" + + USER_ID + ".*"); + } + + @Test + public void testAssignUserToDisplay_userAlreadyAssigned() { + mMediator.assignUserToDisplay(USER_ID, SECONDARY_DISPLAY_ID); + + IllegalStateException e = assertThrows(IllegalStateException.class, + () -> mMediator.assignUserToDisplay(USER_ID, OTHER_SECONDARY_DISPLAY_ID)); + + Log.v(TAG, "Exception: " + e); + assertWithMessage("exception (%s) message", e).that(e).hasMessageThat() + .matches("Cannot.*" + USER_ID + ".*" + OTHER_SECONDARY_DISPLAY_ID + ".*already.*" + + SECONDARY_DISPLAY_ID + ".*"); + + assertUserAssignedToDisplay(USER_ID, SECONDARY_DISPLAY_ID); + } + + @Test + public void testAssignUserToDisplay_profileOnSameDisplayAsParent() { + addDefaultProfileAndParent(); + + mMediator.assignUserToDisplay(PARENT_USER_ID, SECONDARY_DISPLAY_ID); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> mMediator.assignUserToDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID)); + + Log.v(TAG, "Exception: " + e); + assertUserAssignedToDisplay(PARENT_USER_ID, SECONDARY_DISPLAY_ID); + } + + @Test + public void testAssignUserToDisplay_profileOnDifferentDisplayAsParent() { + addDefaultProfileAndParent(); + + mMediator.assignUserToDisplay(PARENT_USER_ID, SECONDARY_DISPLAY_ID); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> mMediator.assignUserToDisplay(PROFILE_USER_ID, OTHER_SECONDARY_DISPLAY_ID)); + + Log.v(TAG, "Exception: " + e); + assertUserAssignedToDisplay(PARENT_USER_ID, SECONDARY_DISPLAY_ID); + } + + @Test + public void testAssignUserToDisplay_profileDefaultDisplayParentOnSecondaryDisplay() { + addDefaultProfileAndParent(); + + mMediator.assignUserToDisplay(PARENT_USER_ID, SECONDARY_DISPLAY_ID); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> mMediator.assignUserToDisplay(PROFILE_USER_ID, DEFAULT_DISPLAY)); + + Log.v(TAG, "Exception: " + e); + assertUserAssignedToDisplay(PARENT_USER_ID, SECONDARY_DISPLAY_ID); + } + + @Test + public void testUnassignUserFromDisplay() { + testAssignUserToDisplay_displayAvailable(); + + mMediator.unassignUserFromDisplay(USER_ID); + + assertNoUserAssignedToDisplay(); + } + + @Test + public void testIsUserVisible_bgUserOnSecondaryDisplay() { + mockCurrentUser(OTHER_USER_ID); + assignUserToDisplay(USER_ID, SECONDARY_DISPLAY_ID); + + assertWithMessage("isUserVisible(%s)", USER_ID) + .that(mMediator.isUserVisible(USER_ID)).isTrue(); + } + + // NOTE: we don't need to add tests for profiles (started / stopped profiles of bg user), as + // isUserVisible() for bg users relies only on the user / display assignments + + @Test + public void testIsUserVisibleOnDisplay_currentUserUnassignedSecondaryDisplay() { + mockCurrentUser(USER_ID); + + assertWithMessage("isUserVisible(%s, %s)", USER_ID, SECONDARY_DISPLAY_ID) + .that(mMediator.isUserVisible(USER_ID, SECONDARY_DISPLAY_ID)).isTrue(); + } + + @Test + public void testIsUserVisibleOnDisplay_currentUserSecondaryDisplayAssignedToAnotherUser() { + mockCurrentUser(USER_ID); + assignUserToDisplay(OTHER_USER_ID, SECONDARY_DISPLAY_ID); + + assertWithMessage("isUserVisible(%s, %s)", USER_ID, SECONDARY_DISPLAY_ID) + .that(mMediator.isUserVisible(USER_ID, SECONDARY_DISPLAY_ID)).isFalse(); + } + + @Test + public void testIsUserVisibleOnDisplay_startedProfileOfCurrentUserSecondaryDisplayAssignedToAnotherUser() { + addDefaultProfileAndParent(); + startDefaultProfile(); + mockCurrentUser(PARENT_USER_ID); + assignUserToDisplay(OTHER_USER_ID, SECONDARY_DISPLAY_ID); + + assertWithMessage("isUserVisible(%s, %s)", PROFILE_USER_ID, SECONDARY_DISPLAY_ID) + .that(mMediator.isUserVisible(PROFILE_USER_ID, SECONDARY_DISPLAY_ID)).isFalse(); + } + + @Test + public void testIsUserVisibleOnDisplay_stoppedProfileOfCurrentUserSecondaryDisplayAssignedToAnotherUser() { + addDefaultProfileAndParent(); + stopDefaultProfile(); + mockCurrentUser(PARENT_USER_ID); + assignUserToDisplay(OTHER_USER_ID, SECONDARY_DISPLAY_ID); + + assertWithMessage("isUserVisible(%s, %s)", PROFILE_USER_ID, SECONDARY_DISPLAY_ID) + .that(mMediator.isUserVisible(PROFILE_USER_ID, SECONDARY_DISPLAY_ID)).isFalse(); + } + + @Test + public void testIsUserVisibleOnDisplay_startedProfileOfCurrentUserOnUnassignedSecondaryDisplay() { + addDefaultProfileAndParent(); + startDefaultProfile(); + mockCurrentUser(PARENT_USER_ID); + + // TODO(b/244644281): change it to isFalse() once isUserVisible() is fixed (see note there) + assertWithMessage("isUserVisible(%s, %s)", PROFILE_USER_ID, SECONDARY_DISPLAY_ID) + .that(mMediator.isUserVisible(PROFILE_USER_ID, SECONDARY_DISPLAY_ID)).isTrue(); + } + + @Test + public void testIsUserVisibleOnDisplay_bgUserOnSecondaryDisplay() { + mockCurrentUser(OTHER_USER_ID); + assignUserToDisplay(USER_ID, SECONDARY_DISPLAY_ID); + + assertWithMessage("isUserVisible(%s, %s)", USER_ID, SECONDARY_DISPLAY_ID) + .that(mMediator.isUserVisible(USER_ID, SECONDARY_DISPLAY_ID)).isTrue(); + } + + @Test + public void testIsUserVisibleOnDisplay_bgUserOnAnotherSecondaryDisplay() { + mockCurrentUser(OTHER_USER_ID); + assignUserToDisplay(USER_ID, SECONDARY_DISPLAY_ID); + + assertWithMessage("isUserVisible(%s, %s)", USER_ID, SECONDARY_DISPLAY_ID) + .that(mMediator.isUserVisible(USER_ID, OTHER_SECONDARY_DISPLAY_ID)).isFalse(); + } + + // NOTE: we don't need to add tests for profiles (started / stopped profiles of bg user), as + // the tests for isUserVisible(userId, display) for non-current users relies on the explicit + // user / display assignments + // TODO(b/244644281): add such tests if the logic change + + @Test + public void testGetDisplayAssignedToUser_bgUserOnSecondaryDisplay() { + mockCurrentUser(OTHER_USER_ID); + assignUserToDisplay(USER_ID, SECONDARY_DISPLAY_ID); + + assertWithMessage("getDisplayAssignedToUser(%s)", USER_ID) + .that(mMediator.getDisplayAssignedToUser(USER_ID)) + .isEqualTo(SECONDARY_DISPLAY_ID); + } + + // NOTE: we don't need to add tests for profiles (started / stopped profiles of bg user), as + // getDisplayAssignedToUser() for bg users relies only on the user / display assignments + + @Test + public void testGetUserAssignedToDisplay_bgUserOnSecondaryDisplay() { + mockCurrentUser(OTHER_USER_ID); + assignUserToDisplay(USER_ID, SECONDARY_DISPLAY_ID); + + assertWithMessage("getUserAssignedToDisplay(%s)", SECONDARY_DISPLAY_ID) + .that(mMediator.getUserAssignedToDisplay(SECONDARY_DISPLAY_ID)).isEqualTo(USER_ID); + } + + @Test + public void testGetUserAssignedToDisplay_noUserOnSecondaryDisplay() { + mockCurrentUser(USER_ID); + + assertWithMessage("getUserAssignedToDisplay(%s)", SECONDARY_DISPLAY_ID) + .that(mMediator.getUserAssignedToDisplay(SECONDARY_DISPLAY_ID)).isEqualTo(USER_ID); + } + + // TODO(b/244644281): scenario below shouldn't happen on "real life", as the profile cannot be + // started on secondary display if its parent isn't, so we might need to remove (or refactor + // this test) if/when the underlying logic changes + @Test + public void testGetUserAssignedToDisplay_profileOnSecondaryDisplay() { + addDefaultProfileAndParent(); + mockCurrentUser(USER_ID); + assignUserToDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID); + + assertWithMessage("getUserAssignedToDisplay(%s)", SECONDARY_DISPLAY_ID) + .that(mMediator.getUserAssignedToDisplay(SECONDARY_DISPLAY_ID)).isEqualTo(USER_ID); + } + + // NOTE: we don't need to add tests for profiles (started / stopped profiles of bg user), as + // getUserAssignedToDisplay() for bg users relies only on the user / display assignments +} diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorSUSDTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorSUSDTest.java new file mode 100644 index 000000000000..7ae811793949 --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorSUSDTest.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.pm; + +import static android.os.UserHandle.USER_SYSTEM; + +import static org.junit.Assert.assertThrows; + +import org.junit.Test; + +/** + * Tests for {@link UserVisibilityMediator} tests for devices that DO NOT support concurrent + * multiple users on multiple displays (A.K.A {@code SUSD} - Single User on Single Device). + * + * <p>Run as + * {@code atest FrameworksMockingServicesTests:com.android.server.pm.UserVisibilityMediatorSUSDTest} + */ +public final class UserVisibilityMediatorSUSDTest extends UserVisibilityMediatorTestCase { + + public UserVisibilityMediatorSUSDTest() { + super(/* usersOnSecondaryDisplaysEnabled= */ false); + } + + @Test + public void testAssignUserToDisplay_otherDisplay_currentUser() { + mockCurrentUser(USER_ID); + + assertThrows(UnsupportedOperationException.class, + () -> mMediator.assignUserToDisplay(USER_ID, SECONDARY_DISPLAY_ID)); + } + + @Test + public void testAssignUserToDisplay_otherDisplay_startProfileOfcurrentUser() { + mockCurrentUser(PARENT_USER_ID); + addDefaultProfileAndParent(); + startDefaultProfile(); + + assertThrows(UnsupportedOperationException.class, + () -> mMediator.assignUserToDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID)); + } + + @Test + public void testAssignUserToDisplay_otherDisplay_stoppedProfileOfcurrentUser() { + mockCurrentUser(PARENT_USER_ID); + addDefaultProfileAndParent(); + stopDefaultProfile(); + + assertThrows(UnsupportedOperationException.class, + () -> mMediator.assignUserToDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID)); + } + + @Test + public void testUnassignUserFromDisplay_ignored() { + mockCurrentUser(USER_ID); + + mMediator.unassignUserFromDisplay(USER_SYSTEM); + mMediator.unassignUserFromDisplay(USER_ID); + mMediator.unassignUserFromDisplay(OTHER_USER_ID); + + assertNoUserAssignedToDisplay(); + } +} diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorTestCase.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorTestCase.java new file mode 100644 index 000000000000..22e6e0dcae1c --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorTestCase.java @@ -0,0 +1,323 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.pm; + +import static android.os.UserHandle.USER_NULL; +import static android.view.Display.DEFAULT_DISPLAY; +import static android.view.Display.INVALID_DISPLAY; + +import static com.android.server.am.UserState.STATE_RUNNING_UNLOCKED; + +import static com.google.common.truth.Truth.assertWithMessage; + +import android.annotation.UserIdInt; +import android.util.SparseIntArray; + +import org.junit.Before; +import org.junit.Test; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Base class for {@link UserVisibilityMediator} tests. + * + * <p>It contains common logics and tests for behaviors that should be invariant regardless of the + * device mode (for example, whether the device supports concurrent multiple users on multiple + * displays or not). + */ +abstract class UserVisibilityMediatorTestCase extends UserManagerServiceOrInternalTestCase { + + /** + * Id of a secondary display (i.e, not {@link android.view.Display.DEFAULT_DISPLAY}). + */ + protected static final int SECONDARY_DISPLAY_ID = 42; + + /** + * Id of another secondary display (i.e, not {@link android.view.Display.DEFAULT_DISPLAY}). + */ + protected static final int OTHER_SECONDARY_DISPLAY_ID = 108; + + private final boolean mUsersOnSecondaryDisplaysEnabled; + + // TODO(b/244644281): manipulating mUsersOnSecondaryDisplays directly leaks implementation + // details into the unit test, but it's fine for now as the tests were copied "as is" - it + // would be better to use a geter() instead + protected final SparseIntArray mUsersOnSecondaryDisplays = new SparseIntArray(); + + protected UserVisibilityMediator mMediator; + + protected UserVisibilityMediatorTestCase(boolean usersOnSecondaryDisplaysEnabled) { + mUsersOnSecondaryDisplaysEnabled = usersOnSecondaryDisplaysEnabled; + } + + @Before + public final void setMediator() { + mMediator = new UserVisibilityMediator(mUms, mUsersOnSecondaryDisplaysEnabled, + mUsersOnSecondaryDisplays); + } + + @Test + public final void testAssignUserToDisplay_defaultDisplayIgnored() { + mMediator.assignUserToDisplay(USER_ID, DEFAULT_DISPLAY); + + assertNoUserAssignedToDisplay(); + } + + @Test + public final void testIsUserVisible_invalidUser() { + mockCurrentUser(USER_ID); + + assertWithMessage("isUserVisible(%s)", USER_NULL) + .that(mMediator.isUserVisible(USER_NULL)).isFalse(); + } + + @Test + public final void testIsUserVisible_currentUser() { + mockCurrentUser(USER_ID); + + assertWithMessage("isUserVisible(%s)", USER_ID) + .that(mMediator.isUserVisible(USER_ID)).isTrue(); + } + + @Test + public final void testIsUserVisible_nonCurrentUser() { + mockCurrentUser(OTHER_USER_ID); + + assertWithMessage("isUserVisible(%s)", USER_ID) + .that(mMediator.isUserVisible(USER_ID)).isFalse(); + } + + @Test + public final void testIsUserVisible_startedProfileOfcurrentUser() { + addDefaultProfileAndParent(); + mockCurrentUser(PARENT_USER_ID); + startDefaultProfile(); + setUserState(PROFILE_USER_ID, STATE_RUNNING_UNLOCKED); + + assertWithMessage("isUserVisible(%s)", PROFILE_USER_ID) + .that(mMediator.isUserVisible(PROFILE_USER_ID)).isTrue(); + } + + @Test + public final void testIsUserVisible_stoppedProfileOfcurrentUser() { + addDefaultProfileAndParent(); + mockCurrentUser(PARENT_USER_ID); + stopDefaultProfile(); + + assertWithMessage("isUserVisible(%s)", PROFILE_USER_ID) + .that(mMediator.isUserVisible(PROFILE_USER_ID)).isFalse(); + } + + @Test + public final void testIsUserVisibleOnDisplay_invalidUser() { + mockCurrentUser(USER_ID); + + assertWithMessage("isUserVisible(%s, %s)", USER_NULL, DEFAULT_DISPLAY) + .that(mMediator.isUserVisible(USER_NULL, DEFAULT_DISPLAY)).isFalse(); + } + + @Test + public final void testIsUserVisibleOnDisplay_currentUserInvalidDisplay() { + mockCurrentUser(USER_ID); + + assertWithMessage("isUserVisible(%s, %s)", USER_ID, INVALID_DISPLAY) + .that(mMediator.isUserVisible(USER_ID, INVALID_DISPLAY)).isFalse(); + } + + @Test + public final void testIsUserVisibleOnDisplay_currentUserDefaultDisplay() { + mockCurrentUser(USER_ID); + + assertWithMessage("isUserVisible(%s, %s)", USER_ID, DEFAULT_DISPLAY) + .that(mMediator.isUserVisible(USER_ID, DEFAULT_DISPLAY)).isTrue(); + } + + @Test + public final void testIsUserVisibleOnDisplay_currentUserSecondaryDisplay() { + mockCurrentUser(USER_ID); + + assertWithMessage("isUserVisible(%s, %s)", USER_ID, SECONDARY_DISPLAY_ID) + .that(mMediator.isUserVisible(USER_ID, SECONDARY_DISPLAY_ID)).isTrue(); + } + + @Test + public final void testIsUserVisibleOnDisplay_nonCurrentUserDefaultDisplay() { + mockCurrentUser(OTHER_USER_ID); + + assertWithMessage("isUserVisible(%s, %s)", USER_ID, DEFAULT_DISPLAY) + .that(mMediator.isUserVisible(USER_ID, DEFAULT_DISPLAY)).isFalse(); + } + + @Test + public final void testIsUserVisibleOnDisplay_startedProfileOfcurrentUserInvalidDisplay() { + addDefaultProfileAndParent(); + mockCurrentUser(PARENT_USER_ID); + startDefaultProfile(); + + assertWithMessage("isUserVisible(%s, %s)", PROFILE_USER_ID, INVALID_DISPLAY) + .that(mMediator.isUserVisible(PROFILE_USER_ID, DEFAULT_DISPLAY)).isTrue(); + } + + @Test + public final void testIsUserVisibleOnDisplay_stoppedProfileOfcurrentUserInvalidDisplay() { + addDefaultProfileAndParent(); + mockCurrentUser(PARENT_USER_ID); + stopDefaultProfile(); + + assertWithMessage("isUserVisible(%s, %s)", PROFILE_USER_ID, INVALID_DISPLAY) + .that(mMediator.isUserVisible(PROFILE_USER_ID, DEFAULT_DISPLAY)).isFalse(); + } + + @Test + public final void testIsUserVisibleOnDisplay_startedProfileOfcurrentUserDefaultDisplay() { + addDefaultProfileAndParent(); + mockCurrentUser(PARENT_USER_ID); + startDefaultProfile(); + setUserState(PROFILE_USER_ID, STATE_RUNNING_UNLOCKED); + + assertWithMessage("isUserVisible(%s, %s)", PROFILE_USER_ID, DEFAULT_DISPLAY) + .that(mMediator.isUserVisible(PROFILE_USER_ID, DEFAULT_DISPLAY)).isTrue(); + } + + @Test + public final void testIsUserVisibleOnDisplay_stoppedProfileOfcurrentUserDefaultDisplay() { + addDefaultProfileAndParent(); + mockCurrentUser(PARENT_USER_ID); + stopDefaultProfile(); + + assertWithMessage("isUserVisible(%s, %s)", PROFILE_USER_ID, DEFAULT_DISPLAY) + .that(mMediator.isUserVisible(PROFILE_USER_ID, DEFAULT_DISPLAY)).isFalse(); + } + + @Test + public final void testIsUserVisibleOnDisplay_startedProfileOfCurrentUserSecondaryDisplay() { + addDefaultProfileAndParent(); + mockCurrentUser(PARENT_USER_ID); + startDefaultProfile(); + setUserState(PROFILE_USER_ID, STATE_RUNNING_UNLOCKED); + + assertWithMessage("isUserVisible(%s, %s)", PROFILE_USER_ID, SECONDARY_DISPLAY_ID) + .that(mMediator.isUserVisible(PROFILE_USER_ID, SECONDARY_DISPLAY_ID)).isTrue(); + } + + @Test + public void testIsUserVisibleOnDisplay_stoppedProfileOfcurrentUserSecondaryDisplay() { + addDefaultProfileAndParent(); + mockCurrentUser(PARENT_USER_ID); + stopDefaultProfile(); + + assertWithMessage("isUserVisible(%s, %s)", PROFILE_USER_ID, SECONDARY_DISPLAY_ID) + .that(mMediator.isUserVisible(PROFILE_USER_ID, SECONDARY_DISPLAY_ID)).isFalse(); + } + + @Test + public void testGetDisplayAssignedToUser_invalidUser() { + mockCurrentUser(USER_ID); + + assertWithMessage("getDisplayAssignedToUser(%s)", USER_NULL) + .that(mMediator.getDisplayAssignedToUser(USER_NULL)).isEqualTo(INVALID_DISPLAY); + } + + @Test + public void testGetDisplayAssignedToUser_currentUser() { + mockCurrentUser(USER_ID); + + assertWithMessage("getDisplayAssignedToUser(%s)", USER_ID) + .that(mMediator.getDisplayAssignedToUser(USER_ID)).isEqualTo(DEFAULT_DISPLAY); + } + + @Test + public final void testGetDisplayAssignedToUser_nonCurrentUser() { + mockCurrentUser(OTHER_USER_ID); + + assertWithMessage("getDisplayAssignedToUser(%s)", USER_ID) + .that(mMediator.getDisplayAssignedToUser(USER_ID)).isEqualTo(INVALID_DISPLAY); + } + + @Test + public final void testGetDisplayAssignedToUser_startedProfileOfcurrentUser() { + addDefaultProfileAndParent(); + mockCurrentUser(PARENT_USER_ID); + startDefaultProfile(); + setUserState(PROFILE_USER_ID, STATE_RUNNING_UNLOCKED); + + assertWithMessage("getDisplayAssignedToUser(%s)", PROFILE_USER_ID) + .that(mMediator.getDisplayAssignedToUser(PROFILE_USER_ID)) + .isEqualTo(DEFAULT_DISPLAY); + } + + @Test + public final void testGetDisplayAssignedToUser_stoppedProfileOfcurrentUser() { + addDefaultProfileAndParent(); + mockCurrentUser(PARENT_USER_ID); + stopDefaultProfile(); + + assertWithMessage("getDisplayAssignedToUser(%s)", PROFILE_USER_ID) + .that(mMediator.getDisplayAssignedToUser(PROFILE_USER_ID)) + .isEqualTo(INVALID_DISPLAY); + } + + @Test + public void testGetUserAssignedToDisplay_invalidDisplay() { + mockCurrentUser(USER_ID); + + assertWithMessage("getUserAssignedToDisplay(%s)", INVALID_DISPLAY) + .that(mMediator.getUserAssignedToDisplay(INVALID_DISPLAY)).isEqualTo(USER_ID); + } + + @Test + public final void testGetUserAssignedToDisplay_defaultDisplay() { + mockCurrentUser(USER_ID); + + assertWithMessage("getUserAssignedToDisplay(%s)", DEFAULT_DISPLAY) + .that(mMediator.getUserAssignedToDisplay(DEFAULT_DISPLAY)).isEqualTo(USER_ID); + } + + @Test + public final void testGetUserAssignedToDisplay_secondaryDisplay() { + mockCurrentUser(USER_ID); + + assertWithMessage("getUserAssignedToDisplay(%s)", SECONDARY_DISPLAY_ID) + .that(mMediator.getUserAssignedToDisplay(SECONDARY_DISPLAY_ID)) + .isEqualTo(USER_ID); + } + + // NOTE: should only called by tests that indirectly needs to check user assignments (like + // isUserVisible), not by tests for the user assignment methods per se. + protected final void assignUserToDisplay(@UserIdInt int userId, int displayId) { + mUsersOnSecondaryDisplays.put(userId, displayId); + } + + protected final void assertNoUserAssignedToDisplay() { + assertWithMessage("mUsersOnSecondaryDisplays()").that(usersOnSecondaryDisplaysAsMap()) + .isEmpty(); + } + + protected final void assertUserAssignedToDisplay(@UserIdInt int userId, int displayId) { + assertWithMessage("mUsersOnSecondaryDisplays()").that(usersOnSecondaryDisplaysAsMap()) + .containsExactly(userId, displayId); + } + + private Map<Integer, Integer> usersOnSecondaryDisplaysAsMap() { + int size = mUsersOnSecondaryDisplays.size(); + Map<Integer, Integer> map = new LinkedHashMap<>(size); + for (int i = 0; i < size; i++) { + map.put(mUsersOnSecondaryDisplays.keyAt(i), mUsersOnSecondaryDisplays.valueAt(i)); + } + return map; + } +} diff --git a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java index 0b776a3e6642..fe92a1dbdac1 100644 --- a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java @@ -24,6 +24,7 @@ import static android.app.ActivityManagerInternal.ALLOW_NON_FULL; import static android.app.ActivityManagerInternal.ALLOW_NON_FULL_IN_PROFILE; import static android.app.ActivityManagerInternal.ALLOW_PROFILES_OR_NON_FULL; import static android.content.pm.PackageManager.PERMISSION_GRANTED; +import static android.os.UserHandle.USER_SYSTEM; import static android.testing.DexmakerShareClassLoaderRule.runWithDexmakerShareClassLoader; import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; @@ -246,7 +247,7 @@ public class UserControllerTest { mUserController.setInitialConfig(/* userSwitchUiEnabled= */ false, /* maxRunningUsers= */ 3, /* delayUserDataLocking= */ false); - mUserController.startUser(TEST_USER_ID, true /* foreground */); + mUserController.startUser(TEST_USER_ID, /* foreground= */ true); verify(mInjector.getWindowManager(), never()).startFreezingScreen(anyInt(), anyInt()); verify(mInjector.getWindowManager(), never()).stopFreezingScreen(); verify(mInjector.getWindowManager(), never()).setSwitchingUser(anyBoolean()); @@ -258,6 +259,8 @@ public class UserControllerTest { assertFalse(mUserController.startUser(TEST_PRE_CREATED_USER_ID, /* foreground= */ true)); // Make sure no intents have been fired for pre-created users. assertTrue(mInjector.mSentIntents.isEmpty()); + + verifyUserNeverAssignedToDisplay(); } @Test @@ -280,6 +283,8 @@ public class UserControllerTest { // binder calls, but their side effects (in this case, that the user is stopped right away) assertWithMessage("wrong binder message calls").that(mInjector.mHandler.getMessageCodes()) .containsExactly(USER_START_MSG); + + verifyUserAssignedToDisplay(TEST_PRE_CREATED_USER_ID, Display.DEFAULT_DISPLAY); } private void startUserAssertions( @@ -303,6 +308,7 @@ public class UserControllerTest { assertEquals("User must be in STATE_BOOTING", UserState.STATE_BOOTING, userState.state); assertEquals("Unexpected old user id", 0, reportMsg.arg1); assertEquals("Unexpected new user id", TEST_USER_ID, reportMsg.arg2); + verifyUserAssignedToDisplay(TEST_USER_ID, Display.DEFAULT_DISPLAY); } @Test @@ -313,6 +319,8 @@ public class UserControllerTest { mUserController.startUserInForeground(NONEXIST_USER_ID); verify(mInjector.getWindowManager(), times(1)).setSwitchingUser(anyBoolean()); verify(mInjector.getWindowManager()).setSwitchingUser(false); + + verifyUserNeverAssignedToDisplay(); } @Test @@ -395,6 +403,7 @@ public class UserControllerTest { verify(mInjector, times(0)).dismissKeyguard(any(), anyString()); verify(mInjector.getWindowManager(), times(1)).stopFreezingScreen(); continueUserSwitchAssertions(TEST_USER_ID, false); + verifyOnUserStarting(USER_SYSTEM, /* visible= */ false); } @Test @@ -403,7 +412,7 @@ public class UserControllerTest { mUserController.setInitialConfig(/* userSwitchUiEnabled= */ true, /* maxRunningUsers= */ 3, /* delayUserDataLocking= */ false); // Start user -- this will update state of mUserController - mUserController.startUser(TEST_USER_ID, true); + mUserController.startUser(TEST_USER_ID, /* foreground=*/ true); Message reportMsg = mInjector.mHandler.getMessageForCode(REPORT_USER_SWITCH_MSG); assertNotNull(reportMsg); UserState userState = (UserState) reportMsg.obj; @@ -415,6 +424,7 @@ public class UserControllerTest { verify(mInjector, times(1)).dismissKeyguard(any(), anyString()); verify(mInjector.getWindowManager(), times(1)).stopFreezingScreen(); continueUserSwitchAssertions(TEST_USER_ID, false); + verifyOnUserStarting(USER_SYSTEM, /* visible= */ false); } @Test @@ -423,7 +433,7 @@ public class UserControllerTest { /* maxRunningUsers= */ 3, /* delayUserDataLocking= */ false); // Start user -- this will update state of mUserController - mUserController.startUser(TEST_USER_ID, true); + mUserController.startUser(TEST_USER_ID, /* foreground=*/ true); Message reportMsg = mInjector.mHandler.getMessageForCode(REPORT_USER_SWITCH_MSG); assertNotNull(reportMsg); UserState userState = (UserState) reportMsg.obj; @@ -521,6 +531,7 @@ public class UserControllerTest { assertFalse(mUserController.canStartMoreUsers()); assertEquals(Arrays.asList(new Integer[] {0, TEST_USER_ID1, TEST_USER_ID2}), mUserController.getRunningUsersLU()); + verifyOnUserStarting(USER_SYSTEM, /* visible= */ false); } /** @@ -530,7 +541,7 @@ public class UserControllerTest { */ @Test public void testUserLockingFromUserSwitchingForMultipleUsersDelayedLockingMode() - throws InterruptedException, RemoteException { + throws Exception { mUserController.setInitialConfig(/* userSwitchUiEnabled= */ true, /* maxRunningUsers= */ 3, /* delayUserDataLocking= */ true); @@ -645,6 +656,8 @@ public class UserControllerTest { setUpUser(TEST_USER_ID1, 0); assertThrows(IllegalArgumentException.class, () -> mUserController.startProfile(TEST_USER_ID1)); + + verifyUserNeverAssignedToDisplay(); } @Test @@ -660,6 +673,8 @@ public class UserControllerTest { setUpUser(TEST_USER_ID1, UserInfo.FLAG_PROFILE | UserInfo.FLAG_DISABLED, /* preCreated= */ false, UserManager.USER_TYPE_PROFILE_MANAGED); assertThat(mUserController.startProfile(TEST_USER_ID1)).isFalse(); + + verifyUserNeverAssignedToDisplay(); } @Test @@ -949,6 +964,10 @@ public class UserControllerTest { verify(mInjector.getUserManagerInternal(), never()).unassignUserFromDisplay(userId); } + private void verifyOnUserStarting(@UserIdInt int userId, boolean visible) { + verify(mInjector).onUserStarting(userId, visible); + } + // Should be public to allow mocking private static class TestInjector extends UserController.Injector { public final TestHandler mHandler; @@ -1084,6 +1103,11 @@ public class UserControllerTest { protected LockPatternUtils getLockPatternUtils() { return mLockPatternUtilsMock; } + + @Override + void onUserStarting(@UserIdInt int userId, boolean visible) { + Log.i(TAG, "onUserStarting(" + userId + ", " + visible + ")"); + } } private static class TestHandler extends Handler { diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/SensorOverlaysTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/SensorOverlaysTest.java index 5012335b533f..21c9c753a062 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/SensorOverlaysTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/SensorOverlaysTest.java @@ -61,7 +61,7 @@ public class SensorOverlaysTest { @Test public void noopWhenBothNull() { - final SensorOverlays useless = new SensorOverlays(null, null); + final SensorOverlays useless = new SensorOverlays(null, null, null); useless.show(SENSOR_ID, 2, null); useless.hide(SENSOR_ID); } @@ -69,12 +69,12 @@ public class SensorOverlaysTest { @Test public void testProvidesUdfps() { final List<IUdfpsOverlayController> udfps = new ArrayList<>(); - SensorOverlays sensorOverlays = new SensorOverlays(null, mSidefpsController); + SensorOverlays sensorOverlays = new SensorOverlays(null, mSidefpsController, null); sensorOverlays.ifUdfps(udfps::add); assertThat(udfps).isEmpty(); - sensorOverlays = new SensorOverlays(mUdfpsOverlayController, mSidefpsController); + sensorOverlays = new SensorOverlays(mUdfpsOverlayController, mSidefpsController, null); sensorOverlays.ifUdfps(udfps::add); assertThat(udfps).containsExactly(mUdfpsOverlayController); } @@ -96,7 +96,7 @@ public class SensorOverlaysTest { private void testShow(IUdfpsOverlayController udfps, ISidefpsController sidefps) throws Exception { - final SensorOverlays sensorOverlays = new SensorOverlays(udfps, sidefps); + final SensorOverlays sensorOverlays = new SensorOverlays(udfps, sidefps, null); final int reason = BiometricOverlayConstants.REASON_UNKNOWN; sensorOverlays.show(SENSOR_ID, reason, mAcquisitionClient); @@ -126,7 +126,7 @@ public class SensorOverlaysTest { private void testHide(IUdfpsOverlayController udfps, ISidefpsController sidefps) throws Exception { - final SensorOverlays sensorOverlays = new SensorOverlays(udfps, sidefps); + final SensorOverlays sensorOverlays = new SensorOverlays(udfps, sidefps, null); sensorOverlays.hide(SENSOR_ID); if (udfps != null) { diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java index 12b8264fc20c..41f743367aeb 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java @@ -39,6 +39,7 @@ import androidx.test.filters.SmallTest; import com.android.server.biometrics.log.BiometricContext; import com.android.server.biometrics.sensors.BiometricScheduler; +import com.android.server.biometrics.sensors.BiometricStateCallback; import com.android.server.biometrics.sensors.HalClientMonitor; import com.android.server.biometrics.sensors.LockoutResetDispatcher; @@ -63,6 +64,8 @@ public class FaceProviderTest { private IFace mDaemon; @Mock private BiometricContext mBiometricContext; + @Mock + private BiometricStateCallback mBiometricStateCallback; private SensorProps[] mSensorProps; private LockoutResetDispatcher mLockoutResetDispatcher; @@ -91,8 +94,8 @@ public class FaceProviderTest { mLockoutResetDispatcher = new LockoutResetDispatcher(mContext); - mFaceProvider = new TestableFaceProvider(mDaemon, mContext, mSensorProps, TAG, - mLockoutResetDispatcher, mBiometricContext); + mFaceProvider = new TestableFaceProvider(mDaemon, mContext, mBiometricStateCallback, + mSensorProps, TAG, mLockoutResetDispatcher, mBiometricContext); } @SuppressWarnings("rawtypes") @@ -140,11 +143,13 @@ public class FaceProviderTest { TestableFaceProvider(@NonNull IFace daemon, @NonNull Context context, + @NonNull BiometricStateCallback biometricStateCallback, @NonNull SensorProps[] props, @NonNull String halInstanceName, @NonNull LockoutResetDispatcher lockoutResetDispatcher, @NonNull BiometricContext biometricContext) { - super(context, props, halInstanceName, lockoutResetDispatcher, biometricContext); + super(context, biometricStateCallback, props, halInstanceName, lockoutResetDispatcher, + biometricContext); mDaemon = daemon; } diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/Face10Test.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/Face10Test.java index 116d2d5a66a0..a2cade7ad797 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/Face10Test.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/Face10Test.java @@ -43,6 +43,7 @@ import androidx.test.filters.SmallTest; import com.android.server.biometrics.log.BiometricContext; import com.android.server.biometrics.sensors.BiometricScheduler; +import com.android.server.biometrics.sensors.BiometricStateCallback; import com.android.server.biometrics.sensors.LockoutResetDispatcher; import org.junit.Before; @@ -73,6 +74,8 @@ public class Face10Test { private BiometricScheduler mScheduler; @Mock private BiometricContext mBiometricContext; + @Mock + private BiometricStateCallback mBiometricStateCallback; private final Handler mHandler = new Handler(Looper.getMainLooper()); private LockoutResetDispatcher mLockoutResetDispatcher; @@ -103,8 +106,8 @@ public class Face10Test { resetLockoutRequiresChallenge); Face10.sSystemClock = Clock.fixed(Instant.ofEpochMilli(100), ZoneId.of("PST")); - mFace10 = new Face10(mContext, sensorProps, mLockoutResetDispatcher, mHandler, mScheduler, - mBiometricContext); + mFace10 = new Face10(mContext, mBiometricStateCallback, sensorProps, + mLockoutResetDispatcher, mHandler, mScheduler, mBiometricContext); mBinder = new Binder(); } diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java index 1b5db0a35449..675f0e357aa8 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java @@ -645,7 +645,7 @@ public class FingerprintAuthenticationClientTest { 9 /* sensorId */, mBiometricLogger, mBiometricContext, true /* isStrongBiometric */, null /* taskStackListener */, mLockoutCache, - mUdfpsOverlayController, mSideFpsController, allowBackgroundAuthentication, + mUdfpsOverlayController, mSideFpsController, null, allowBackgroundAuthentication, mSensorProps, new Handler(mLooper.getLooper()), 0 /* biometricStrength */, mClock) { @Override diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClientTest.java index 93cbef19aca9..4579fc15f661 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClientTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClientTest.java @@ -118,6 +118,6 @@ public class FingerprintDetectClientTest { return new FingerprintDetectClient(mContext, () -> aidl, mToken, 6 /* requestId */, mClientMonitorCallbackConverter, 2 /* userId */, "a-test", 1 /* sensorId */, mBiometricLogger, mBiometricContext, - mUdfpsOverlayController, true /* isStrongBiometric */); + mUdfpsOverlayController, null, true /* isStrongBiometric */); } } diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClientTest.java index 837b55397416..38b06c476174 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClientTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClientTest.java @@ -28,7 +28,6 @@ import static org.mockito.Mockito.any; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.never; import static org.mockito.Mockito.same; -import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -66,7 +65,6 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; -import java.util.ArrayList; import java.util.function.Consumer; @Presubmit @@ -292,6 +290,6 @@ public class FingerprintEnrollClientTest { mClientMonitorCallbackConverter, 0 /* userId */, HAT, "owner", mBiometricUtils, 8 /* sensorId */, mBiometricLogger, mBiometricContext, mSensorProps, mUdfpsOverlayController, - mSideFpsController, 6 /* maxTemplatesPerUser */, FingerprintManager.ENROLL_ENROLL); + mSideFpsController, null, 6 /* maxTemplatesPerUser */, FingerprintManager.ENROLL_ENROLL); } } diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java index 02bbe658f9b2..5fda3d6b36ab 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java @@ -891,4 +891,34 @@ public class VirtualDeviceManagerServiceTest { verify(mContext).startActivityAsUser(argThat(intent -> intent.filterEquals(blockedAppIntent)), any(), any()); } + + @Test + public void registerRunningAppsChangedListener_onRunningAppsChanged_listenersNotified() { + ArraySet<Integer> uids = new ArraySet<>(Arrays.asList(UID_1, UID_2)); + mDeviceImpl.onVirtualDisplayCreatedLocked( + mDeviceImpl.createWindowPolicyController(), DISPLAY_ID); + GenericWindowPolicyController gwpc = mDeviceImpl.getWindowPolicyControllersForTesting().get( + DISPLAY_ID); + + gwpc.onRunningAppsChanged(uids); + mDeviceImpl.onRunningAppsChanged(uids); + + assertThat(gwpc.getRunningAppsChangedListenersSizeForTesting()).isEqualTo(1); + verify(mRunningAppsChangedCallback).accept(new ArraySet<>(Arrays.asList(UID_1, UID_2))); + } + + @Test + public void noRunningAppsChangedListener_onRunningAppsChanged_doesNotThrowException() { + ArraySet<Integer> uids = new ArraySet<>(Arrays.asList(UID_1, UID_2)); + mDeviceImpl.onVirtualDisplayCreatedLocked( + mDeviceImpl.createWindowPolicyController(), DISPLAY_ID); + GenericWindowPolicyController gwpc = mDeviceImpl.getWindowPolicyControllersForTesting().get( + DISPLAY_ID); + mDeviceImpl.onVirtualDisplayRemovedLocked(DISPLAY_ID); + + // This call should not throw any exceptions. + gwpc.onRunningAppsChanged(uids); + + assertThat(gwpc.getRunningAppsChangedListenersSizeForTesting()).isEqualTo(0); + } } diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java index 7a2a5838134c..54baf18da92a 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java @@ -590,11 +590,15 @@ public class HdmiCecLocalDeviceTvTest { @Test public void handleReportAudioStatus_SamOnArcOff_setStreamVolumeNotCalled() { + mHdmiControlService.getHdmiCecConfig().setIntValue( + HdmiControlManager.CEC_SETTING_NAME_SYSTEM_AUDIO_CONTROL, + HdmiControlManager.SYSTEM_AUDIO_CONTROL_ENABLED); // Emulate Audio device on port 0x1000 (does not support ARC) mNativeWrapper.setPortConnectionStatus(1, true); HdmiCecMessage hdmiCecMessage = HdmiCecMessageBuilder.buildReportPhysicalAddressCommand( ADDR_AUDIO_SYSTEM, 0x1000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM); mNativeWrapper.onCecMessage(hdmiCecMessage); + mTestLooper.dispatchAll(); HdmiCecFeatureAction systemAudioAutoInitiationAction = new SystemAudioAutoInitiationAction(mHdmiCecLocalDeviceTv, ADDR_AUDIO_SYSTEM); @@ -821,6 +825,10 @@ public class HdmiCecLocalDeviceTvTest { @Test public void receiveSetAudioVolumeLevel_samActivated_respondsFeatureAbort_noVolumeChange() { + mHdmiControlService.getHdmiCecConfig().setIntValue( + HdmiControlManager.CEC_SETTING_NAME_SYSTEM_AUDIO_CONTROL, + HdmiControlManager.SYSTEM_AUDIO_CONTROL_ENABLED); + mNativeWrapper.onCecMessage(HdmiCecMessageBuilder.buildSetSystemAudioMode( ADDR_AUDIO_SYSTEM, ADDR_TV, true)); mTestLooper.dispatchAll(); diff --git a/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt b/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt index c68db3460dac..6590a2b500e4 100644 --- a/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt +++ b/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt @@ -36,6 +36,7 @@ import androidx.test.InstrumentationRegistry import com.android.server.input.BatteryController.POLLING_PERIOD_MILLIS import com.android.server.input.BatteryController.UEventManager import com.android.server.input.BatteryController.UEventManager.UEventBatteryListener +import com.android.server.input.BatteryController.USI_BATTERY_VALIDITY_DURATION_MILLIS import org.hamcrest.Description import org.hamcrest.Matcher import org.hamcrest.MatcherAssert.assertThat @@ -528,10 +529,109 @@ class BatteryControllerTests { matchesState(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.64f)) // The battery is no longer present after the timeout expires. - testLooper.moveTimeForward(BatteryController.USI_BATTERY_VALIDITY_DURATION_MILLIS - 1) + testLooper.moveTimeForward(USI_BATTERY_VALIDITY_DURATION_MILLIS - 1) testLooper.dispatchNext() listener.verifyNotified(isInvalidBatteryState(USI_DEVICE_ID), times(2)) assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID), isInvalidBatteryState(USI_DEVICE_ID)) } + + @Test + fun testStylusPresenceExtendsValidUsiBatteryState() { + `when`(native.getBatteryDevicePath(USI_DEVICE_ID)).thenReturn("/sys/dev/usi_device") + `when`(native.getBatteryStatus(USI_DEVICE_ID)).thenReturn(STATUS_DISCHARGING) + `when`(native.getBatteryCapacity(USI_DEVICE_ID)).thenReturn(78) + + addInputDevice(USI_DEVICE_ID, supportsUsi = true) + testLooper.dispatchNext() + val uEventListener = ArgumentCaptor.forClass(UEventBatteryListener::class.java) + verify(uEventManager) + .addListener(uEventListener.capture(), eq("DEVPATH=/dev/usi_device")) + + // There is a UEvent signaling a battery change. The battery state is now valid. + uEventListener.value!!.onBatteryUEvent(TIMESTAMP) + val listener = createMockListener() + batteryController.registerBatteryListener(USI_DEVICE_ID, listener, PID) + listener.verifyNotified(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.78f) + assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID), + matchesState(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.78f)) + + // Stylus presence is detected before the validity timeout expires. + testLooper.moveTimeForward(100) + testLooper.dispatchAll() + batteryController.notifyStylusGestureStarted(USI_DEVICE_ID, TIMESTAMP) + + // Ensure that timeout was extended, and the battery state is now valid for longer. + testLooper.moveTimeForward(USI_BATTERY_VALIDITY_DURATION_MILLIS - 100) + testLooper.dispatchAll() + assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID), + matchesState(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.78f)) + + // Ensure the validity period expires after the expected amount of time. + testLooper.moveTimeForward(100) + testLooper.dispatchNext() + listener.verifyNotified(isInvalidBatteryState(USI_DEVICE_ID)) + assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID), + isInvalidBatteryState(USI_DEVICE_ID)) + } + + @Test + fun testStylusPresenceMakesUsiBatteryStateValid() { + `when`(native.getBatteryDevicePath(USI_DEVICE_ID)).thenReturn("/sys/dev/usi_device") + `when`(native.getBatteryStatus(USI_DEVICE_ID)).thenReturn(STATUS_DISCHARGING) + `when`(native.getBatteryCapacity(USI_DEVICE_ID)).thenReturn(78) + + addInputDevice(USI_DEVICE_ID, supportsUsi = true) + testLooper.dispatchNext() + val uEventListener = ArgumentCaptor.forClass(UEventBatteryListener::class.java) + verify(uEventManager) + .addListener(uEventListener.capture(), eq("DEVPATH=/dev/usi_device")) + + // The USI battery state is initially invalid. + val listener = createMockListener() + batteryController.registerBatteryListener(USI_DEVICE_ID, listener, PID) + listener.verifyNotified(isInvalidBatteryState(USI_DEVICE_ID)) + assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID), + isInvalidBatteryState(USI_DEVICE_ID)) + + // A stylus presence is detected. This validates the battery state. + batteryController.notifyStylusGestureStarted(USI_DEVICE_ID, TIMESTAMP) + + listener.verifyNotified(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.78f) + assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID), + matchesState(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.78f)) + } + + @Test + fun testStylusPresenceDoesNotMakeUsiBatteryStateValidAtBoot() { + `when`(native.getBatteryDevicePath(USI_DEVICE_ID)).thenReturn("/sys/dev/usi_device") + // At boot, the USI device always reports a capacity value of 0. + `when`(native.getBatteryStatus(USI_DEVICE_ID)).thenReturn(STATUS_UNKNOWN) + `when`(native.getBatteryCapacity(USI_DEVICE_ID)).thenReturn(0) + + addInputDevice(USI_DEVICE_ID, supportsUsi = true) + testLooper.dispatchNext() + val uEventListener = ArgumentCaptor.forClass(UEventBatteryListener::class.java) + verify(uEventManager) + .addListener(uEventListener.capture(), eq("DEVPATH=/dev/usi_device")) + + // The USI battery state is initially invalid. + val listener = createMockListener() + batteryController.registerBatteryListener(USI_DEVICE_ID, listener, PID) + listener.verifyNotified(isInvalidBatteryState(USI_DEVICE_ID)) + assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID), + isInvalidBatteryState(USI_DEVICE_ID)) + + // Since the capacity reported is 0, stylus presence does not validate the battery state. + batteryController.notifyStylusGestureStarted(USI_DEVICE_ID, TIMESTAMP) + + assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID), + isInvalidBatteryState(USI_DEVICE_ID)) + + // However, if a UEvent reports a battery capacity of 0, the battery state is now valid. + uEventListener.value!!.onBatteryUEvent(TIMESTAMP) + listener.verifyNotified(USI_DEVICE_ID, status = STATUS_UNKNOWN, capacity = 0f) + assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID), + matchesState(USI_DEVICE_ID, status = STATUS_UNKNOWN, capacity = 0f)) + } } diff --git a/services/tests/servicestests/src/com/android/server/job/WorkTypeConfigTest.java b/services/tests/servicestests/src/com/android/server/job/WorkTypeConfigTest.java index a5fedef23e00..21d27848bc57 100644 --- a/services/tests/servicestests/src/com/android/server/job/WorkTypeConfigTest.java +++ b/services/tests/servicestests/src/com/android/server/job/WorkTypeConfigTest.java @@ -36,7 +36,6 @@ import androidx.test.runner.AndroidJUnit4; import com.android.server.job.JobConcurrencyManager.WorkTypeConfig; -import org.junit.After; import org.junit.Test; import org.junit.runner.RunWith; @@ -59,30 +58,6 @@ public class WorkTypeConfigTest { private static final String KEY_MIN_BGUSER_IMPORTANT = "concurrency_min_bguser_important_test"; private static final String KEY_MIN_BGUSER = "concurrency_min_bguser_test"; - @After - public void tearDown() throws Exception { - resetConfig(); - } - - private void resetConfig() { - // DeviceConfig.resetToDefaults() doesn't work here. Need to reset constants manually. - DeviceConfig.setProperty(DeviceConfig.NAMESPACE_JOB_SCHEDULER, KEY_MAX_TOTAL, null, false); - DeviceConfig.setProperty(DeviceConfig.NAMESPACE_JOB_SCHEDULER, KEY_MAX_TOP, null, false); - DeviceConfig.setProperty(DeviceConfig.NAMESPACE_JOB_SCHEDULER, KEY_MAX_FGS, null, false); - DeviceConfig.setProperty(DeviceConfig.NAMESPACE_JOB_SCHEDULER, KEY_MAX_EJ, null, false); - DeviceConfig.setProperty(DeviceConfig.NAMESPACE_JOB_SCHEDULER, KEY_MAX_BG, null, false); - DeviceConfig.setProperty(DeviceConfig.NAMESPACE_JOB_SCHEDULER, - KEY_MAX_BGUSER_IMPORTANT, null, false); - DeviceConfig.setProperty(DeviceConfig.NAMESPACE_JOB_SCHEDULER, KEY_MAX_BGUSER, null, false); - DeviceConfig.setProperty(DeviceConfig.NAMESPACE_JOB_SCHEDULER, KEY_MIN_TOP, null, false); - DeviceConfig.setProperty(DeviceConfig.NAMESPACE_JOB_SCHEDULER, KEY_MIN_FGS, null, false); - DeviceConfig.setProperty(DeviceConfig.NAMESPACE_JOB_SCHEDULER, KEY_MIN_EJ, null, false); - DeviceConfig.setProperty(DeviceConfig.NAMESPACE_JOB_SCHEDULER, KEY_MIN_BG, null, false); - DeviceConfig.setProperty(DeviceConfig.NAMESPACE_JOB_SCHEDULER, - KEY_MIN_BGUSER_IMPORTANT, null, false); - DeviceConfig.setProperty(DeviceConfig.NAMESPACE_JOB_SCHEDULER, KEY_MIN_BGUSER, null, false); - } - private void check(@Nullable DeviceConfig.Properties config, int defaultTotal, @NonNull List<Pair<Integer, Integer>> defaultMin, @@ -90,10 +65,6 @@ public class WorkTypeConfigTest { boolean expectedValid, int expectedTotal, @NonNull List<Pair<Integer, Integer>> expectedMinLimits, @NonNull List<Pair<Integer, Integer>> expectedMaxLimits) throws Exception { - resetConfig(); - if (config != null) { - DeviceConfig.setProperties(config); - } final WorkTypeConfig counts; try { @@ -112,7 +83,9 @@ public class WorkTypeConfigTest { } } - counts.update(DeviceConfig.getProperties(DeviceConfig.NAMESPACE_JOB_SCHEDULER)); + if (config != null) { + counts.update(config); + } assertEquals(expectedTotal, counts.getMaxTotal()); for (Pair<Integer, Integer> min : expectedMinLimits) { diff --git a/services/tests/servicestests/src/com/android/server/locales/LocaleManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/locales/LocaleManagerServiceTest.java index dbcd38c35958..dc9f90737713 100644 --- a/services/tests/servicestests/src/com/android/server/locales/LocaleManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/locales/LocaleManagerServiceTest.java @@ -229,6 +229,29 @@ public class LocaleManagerServiceTest { } } + @Test(expected = SecurityException.class) + public void testGetApplicationLocales_currentImeQueryNonForegroundAppLocales_fails() + throws Exception { + doReturn(DEFAULT_UID).when(mMockPackageManager) + .getPackageUidAsUser(anyString(), any(), anyInt()); + doReturn(new PackageConfig(/* nightMode = */ 0, DEFAULT_LOCALES)) + .when(mMockActivityTaskManager).getApplicationConfig(anyString(), anyInt()); + String imPkgName = getCurrentInputMethodPackageName(); + doReturn(Binder.getCallingUid()).when(mMockPackageManager) + .getPackageUidAsUser(eq(imPkgName), any(), anyInt()); + doReturn(false).when(mMockActivityManager).isAppForeground(anyInt()); + setUpFailingPermissionCheckFor(Manifest.permission.READ_APP_SPECIFIC_LOCALES); + + try { + mLocaleManagerService.getApplicationLocales(DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID); + fail("Expected SecurityException"); + } finally { + verify(mMockContext).enforceCallingOrSelfPermission( + eq(android.Manifest.permission.READ_APP_SPECIFIC_LOCALES), + anyString()); + } + } + @Test public void testGetApplicationLocales_appSpecificConfigAbsent_returnsEmptyList() throws Exception { @@ -307,7 +330,7 @@ public class LocaleManagerServiceTest { } @Test - public void testGetApplicationLocales_callerIsCurrentInputMethod_returnsLocales() + public void testGetApplicationLocales_currentImeQueryForegroundAppLocales_returnsLocales() throws Exception { doReturn(DEFAULT_UID).when(mMockPackageManager) .getPackageUidAsUser(anyString(), any(), anyInt()); @@ -316,6 +339,7 @@ public class LocaleManagerServiceTest { String imPkgName = getCurrentInputMethodPackageName(); doReturn(Binder.getCallingUid()).when(mMockPackageManager) .getPackageUidAsUser(eq(imPkgName), any(), anyInt()); + doReturn(true).when(mMockActivityManager).isAppForeground(anyInt()); LocaleList locales = mLocaleManagerService.getApplicationLocales( diff --git a/services/tests/servicestests/src/com/android/server/pm/AppsFilterImplTest.java b/services/tests/servicestests/src/com/android/server/pm/AppsFilterImplTest.java index c321639ffe2e..1a8ef9e53593 100644 --- a/services/tests/servicestests/src/com/android/server/pm/AppsFilterImplTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/AppsFilterImplTest.java @@ -387,7 +387,7 @@ public class AppsFilterImplTest { // delete user when(mSnapshot.getUserInfos()).thenReturn(USER_INFO_LIST); - appsFilter.onUserDeleted(ADDED_USER); + appsFilter.onUserDeleted(mSnapshot, ADDED_USER); for (int subjectUserId : USER_ARRAY) { for (int otherUserId : USER_ARRAY) { @@ -925,7 +925,7 @@ public class AppsFilterImplTest { assertTrue(appsFilter.shouldFilterApplication(mSnapshot, DUMMY_OVERLAY_APPID, overlaySetting, actorSetting, SYSTEM_USER)); - appsFilter.removePackage(mSnapshot, targetSetting, false /* isReplace */); + appsFilter.removePackage(mSnapshot, targetSetting); // Actor loses visibility to the overlay via removal of the target assertTrue(appsFilter.shouldFilterApplication(mSnapshot, DUMMY_ACTOR_APPID, actorSetting, @@ -1267,7 +1267,7 @@ public class AppsFilterImplTest { watcher.verifyNoChangeReported("get"); // remove a package - appsFilter.removePackage(mSnapshot, seesNothing, false /* isReplace */); + appsFilter.removePackage(mSnapshot, seesNothing); watcher.verifyChangeReported("removePackage"); } @@ -1337,7 +1337,7 @@ public class AppsFilterImplTest { target.getPackageName())); // New changes don't affect the snapshot - appsFilter.removePackage(mSnapshot, target, false); + appsFilter.removePackage(mSnapshot, target); assertTrue( appsFilter.shouldFilterApplication(mSnapshot, DUMMY_CALLING_APPID, instrumentation, target, diff --git a/services/tests/servicestests/src/com/android/server/utils/EventLoggerTest.java b/services/tests/servicestests/src/com/android/server/utils/EventLoggerTest.java index 0b27f8734f23..4762696d7c61 100644 --- a/services/tests/servicestests/src/com/android/server/utils/EventLoggerTest.java +++ b/services/tests/servicestests/src/com/android/server/utils/EventLoggerTest.java @@ -125,7 +125,7 @@ public class EventLoggerTest { @Test public void testThatLoggingWorksAsExpected() { for (EventLogger.Event event: mEventsToInsert) { - mEventLogger.log(event); + mEventLogger.enqueue(event); } mEventLogger.dump(mTestPrintWriter); diff --git a/services/tests/uiservicestests/src/com/android/server/slice/SliceManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/slice/SliceManagerServiceTest.java index a917c57446a9..6ef5b0ecb2a0 100644 --- a/services/tests/uiservicestests/src/com/android/server/slice/SliceManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/slice/SliceManagerServiceTest.java @@ -51,6 +51,7 @@ import com.android.server.UiServiceTestCase; import org.junit.After; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; @@ -87,6 +88,7 @@ public class SliceManagerServiceTest extends UiServiceTestCase { LocalServices.removeServiceForTest(UsageStatsManagerInternal.class); } + @Ignore("b/253871109") @Test public void testAddPinCreatesPinned() throws RemoteException { grantSlicePermission(); @@ -97,6 +99,7 @@ public class SliceManagerServiceTest extends UiServiceTestCase { verify(mService, times(1)).createPinnedSlice(eq(maybeAddUserId(TEST_URI, 0)), anyString()); } + @Ignore("b/253871109") @Test public void testRemovePinDestroysPinned() throws RemoteException { grantSlicePermission(); @@ -109,6 +112,7 @@ public class SliceManagerServiceTest extends UiServiceTestCase { verify(mCreatedSliceState, never()).destroy(); } + @Ignore("b/253871109") @Test public void testCheckAutoGrantPermissions() throws RemoteException { String[] testPerms = new String[] { @@ -128,12 +132,14 @@ public class SliceManagerServiceTest extends UiServiceTestCase { verify(mContextSpy).checkPermission(eq("perm2"), eq(Process.myPid()), eq(Process.myUid())); } + @Ignore("b/253871109") @Test(expected = IllegalStateException.class) public void testNoPinThrow() throws Exception { grantSlicePermission(); mService.getPinnedSpecs(TEST_URI, "pkg"); } + @Ignore("b/253871109") @Test public void testGetPinnedSpecs() throws Exception { grantSlicePermission(); diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java index 8a18912b8b68..4ec9762e8399 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java @@ -49,6 +49,7 @@ import static android.os.InputConstants.DEFAULT_DISPATCHING_TIMEOUT_MILLIS; import static android.os.Process.NOBODY_UID; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.InsetsState.ITYPE_IME; +import static android.view.WindowInsets.Type.ime; import static android.view.WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW; import static android.view.WindowManager.LayoutParams.FIRST_SUB_WINDOW; import static android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; @@ -146,7 +147,6 @@ import android.view.IWindowManager; import android.view.IWindowSession; import android.view.InsetsSource; import android.view.InsetsState; -import android.view.InsetsVisibilities; import android.view.RemoteAnimationAdapter; import android.view.RemoteAnimationTarget; import android.view.Surface; @@ -2002,7 +2002,7 @@ public class ActivityRecordTests extends WindowTestsBase { doReturn(WindowManagerGlobal.ADD_STARTING_NOT_NEEDED).when(session).addToDisplay( any() /* window */, any() /* attrs */, anyInt() /* viewVisibility */, anyInt() /* displayId */, - any() /* requestedVisibilities */, any() /* outInputChannel */, + anyInt() /* requestedVisibleTypes */, any() /* outInputChannel */, any() /* outInsetsState */, any() /* outActiveControls */, any() /* outAttachedFrame */, any() /* outSizeCompatScale */); mAtm.mWindowManager.mStartingSurfaceController @@ -3233,9 +3233,7 @@ public class ActivityRecordTests extends WindowTestsBase { app2.mActivityRecord.commitVisibility(false, false); // app1 requests IME visible. - final InsetsVisibilities requestedVisibilities = new InsetsVisibilities(); - requestedVisibilities.setVisibility(ITYPE_IME, true); - app1.setRequestedVisibilities(requestedVisibilities); + app1.setRequestedVisibleTypes(ime(), ime()); mDisplayContent.getInsetsStateController().onInsetsModified(app1); // Verify app1's IME insets is visible and app2's IME insets frozen flag set. diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java index e69418ba908e..37ab9a00737f 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java @@ -34,13 +34,14 @@ import static android.view.Display.FLAG_PRIVATE; import static android.view.Display.INVALID_DISPLAY; import static android.view.DisplayCutout.BOUNDS_POSITION_TOP; import static android.view.DisplayCutout.fromBoundingRect; -import static android.view.InsetsState.ITYPE_IME; -import static android.view.InsetsState.ITYPE_NAVIGATION_BAR; import static android.view.InsetsState.ITYPE_STATUS_BAR; import static android.view.Surface.ROTATION_0; import static android.view.Surface.ROTATION_180; import static android.view.Surface.ROTATION_270; import static android.view.Surface.ROTATION_90; +import static android.view.WindowInsets.Type.ime; +import static android.view.WindowInsets.Type.navigationBars; +import static android.view.WindowInsets.Type.statusBars; import static android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE; import static android.view.WindowManager.LayoutParams.FIRST_SUB_WINDOW; import static android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; @@ -130,7 +131,6 @@ import android.view.IDisplayChangeWindowController; import android.view.ISystemGestureExclusionListener; import android.view.IWindowManager; import android.view.InsetsState; -import android.view.InsetsVisibilities; import android.view.MotionEvent; import android.view.Surface; import android.view.SurfaceControl; @@ -1399,10 +1399,7 @@ public class DisplayContentTests extends WindowTestsBase { win.getAttrs().layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; win.getAttrs().privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION; win.getAttrs().insetsFlags.behavior = BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE; - final InsetsVisibilities requestedVisibilities = new InsetsVisibilities(); - requestedVisibilities.setVisibility(ITYPE_NAVIGATION_BAR, false); - requestedVisibilities.setVisibility(ITYPE_STATUS_BAR, false); - win.setRequestedVisibilities(requestedVisibilities); + win.setRequestedVisibleTypes(0, navigationBars() | statusBars()); win.mActivityRecord.mTargetSdk = P; performLayout(dc); @@ -2486,7 +2483,7 @@ public class DisplayContentTests extends WindowTestsBase { createWindow(null, TYPE_BASE_APPLICATION, mDisplayContent, "imeAppTarget"); mDisplayContent.setImeLayeringTarget(imeAppTarget); spyOn(imeAppTarget); - doReturn(true).when(imeAppTarget).getRequestedVisibility(ITYPE_IME); + doReturn(true).when(imeAppTarget).isRequestedVisible(ime()); assertEquals(imeChildWindow, mDisplayContent.findFocusedWindow()); // Verify imeChildWindow doesn't be focused window if the next IME target does not @@ -2511,7 +2508,7 @@ public class DisplayContentTests extends WindowTestsBase { createWindow(null, TYPE_BASE_APPLICATION, mDisplayContent, "imeAppTarget"); mDisplayContent.setImeLayeringTarget(imeAppTarget); spyOn(imeAppTarget); - doReturn(true).when(imeAppTarget).getRequestedVisibility(ITYPE_IME); + doReturn(true).when(imeAppTarget).isRequestedVisible(ime()); assertEquals(imeMenuDialog, mDisplayContent.findFocusedWindow()); // Verify imeMenuDialog doesn't be focused window if the next IME target is closing. diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java index a980765711d1..52af8adcf9f6 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java @@ -371,7 +371,7 @@ public class DisplayPolicyTests extends WindowTestsBase { final InsetsControlTarget controlTarget = mock(InsetsControlTarget.class); when(provider.getControlTarget()).thenReturn(controlTarget); when(windowState.getControllableInsetProvider()).thenReturn(provider); - when(controlTarget.getRequestedVisibility(anyInt())).thenReturn(true); + when(controlTarget.isRequestedVisible(anyInt())).thenReturn(true); displayPolicy.setCanSystemBarsBeShownByUser(false); displayPolicy.requestTransientBars(windowState, true); diff --git a/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java index f2bc47dfd6e9..fd2a1d1c352f 100644 --- a/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java @@ -19,11 +19,15 @@ package com.android.server.wm; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; +import static android.view.InsetsState.ITYPE_BOTTOM_MANDATORY_GESTURES; +import static android.view.InsetsState.ITYPE_BOTTOM_TAPPABLE_ELEMENT; import static android.view.InsetsState.ITYPE_NAVIGATION_BAR; import static android.view.InsetsState.ITYPE_STATUS_BAR; import static android.view.InsetsState.ITYPE_TOP_MANDATORY_GESTURES; import static android.view.InsetsState.ITYPE_TOP_TAPPABLE_ELEMENT; import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; +import static android.view.WindowInsets.Type.navigationBars; +import static android.view.WindowInsets.Type.statusBars; import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_FORCE_SHOW_STATUS_BAR; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_STATUS_FORCE_SHOW_NAVIGATION; @@ -51,7 +55,7 @@ import android.view.InsetsFrameProvider; import android.view.InsetsSource; import android.view.InsetsSourceControl; import android.view.InsetsState; -import android.view.InsetsVisibilities; +import android.view.WindowInsets; import androidx.test.filters.SmallTest; @@ -74,7 +78,7 @@ public class InsetsPolicyTest extends WindowTestsBase { @Test public void testControlsForDispatch_regular() { addStatusBar(); - addWindow(TYPE_NAVIGATION_BAR, "navBar"); + addNavigationBar(); final InsetsSourceControl[] controls = addAppWindowAndGetControlsForDispatch(); @@ -86,7 +90,7 @@ public class InsetsPolicyTest extends WindowTestsBase { @Test public void testControlsForDispatch_multiWindowTaskVisible() { addStatusBar(); - addWindow(TYPE_NAVIGATION_BAR, "navBar"); + addNavigationBar(); final WindowState win = createWindow(null, WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD, TYPE_APPLICATION, mDisplayContent, "app"); @@ -99,7 +103,7 @@ public class InsetsPolicyTest extends WindowTestsBase { @Test public void testControlsForDispatch_freeformTaskVisible() { addStatusBar(); - addWindow(TYPE_NAVIGATION_BAR, "navBar"); + addNavigationBar(); final WindowState win = createWindow(null, WINDOWING_MODE_FREEFORM, ACTIVITY_TYPE_STANDARD, TYPE_APPLICATION, mDisplayContent, "app"); @@ -112,7 +116,7 @@ public class InsetsPolicyTest extends WindowTestsBase { @Test public void testControlsForDispatch_forceStatusBarVisible() { addStatusBar().mAttrs.privateFlags |= PRIVATE_FLAG_FORCE_SHOW_STATUS_BAR; - addWindow(TYPE_NAVIGATION_BAR, "navBar"); + addNavigationBar(); final InsetsSourceControl[] controls = addAppWindowAndGetControlsForDispatch(); @@ -126,7 +130,7 @@ public class InsetsPolicyTest extends WindowTestsBase { addWindow(TYPE_NOTIFICATION_SHADE, "notificationShade").mAttrs.privateFlags |= PRIVATE_FLAG_STATUS_FORCE_SHOW_NAVIGATION; addStatusBar(); - addWindow(TYPE_NAVIGATION_BAR, "navBar"); + addNavigationBar(); final InsetsSourceControl[] controls = addAppWindowAndGetControlsForDispatch(); @@ -139,7 +143,7 @@ public class InsetsPolicyTest extends WindowTestsBase { public void testControlsForDispatch_statusBarForceShowNavigation_butFocusedAnyways() { WindowState notifShade = addWindow(TYPE_NOTIFICATION_SHADE, "notificationShade"); notifShade.mAttrs.privateFlags |= PRIVATE_FLAG_STATUS_FORCE_SHOW_NAVIGATION; - addWindow(TYPE_NAVIGATION_BAR, "navBar"); + addNavigationBar(); mDisplayContent.getInsetsPolicy().updateBarControlTarget(notifShade); InsetsSourceControl[] controls @@ -155,7 +159,7 @@ public class InsetsPolicyTest extends WindowTestsBase { mDisplayContent.setRemoteInsetsController(createDisplayWindowInsetsController()); mDisplayContent.getInsetsPolicy().setRemoteInsetsControllerControlsSystemBars(true); addStatusBar(); - addWindow(TYPE_NAVIGATION_BAR, "navBar"); + addNavigationBar(); final InsetsSourceControl[] controls = addAppWindowAndGetControlsForDispatch(); @@ -166,13 +170,11 @@ public class InsetsPolicyTest extends WindowTestsBase { @Test public void testControlsForDispatch_topAppHidesStatusBar() { addStatusBar(); - addWindow(TYPE_NAVIGATION_BAR, "navBar"); + addNavigationBar(); // Add a fullscreen (MATCH_PARENT x MATCH_PARENT) app window which hides status bar. final WindowState fullscreenApp = addWindow(TYPE_APPLICATION, "fullscreenApp"); - final InsetsVisibilities requestedVisibilities = new InsetsVisibilities(); - requestedVisibilities.setVisibility(ITYPE_STATUS_BAR, false); - fullscreenApp.setRequestedVisibilities(requestedVisibilities); + fullscreenApp.setRequestedVisibleTypes(0, WindowInsets.Type.statusBars()); // Add a non-fullscreen dialog window. final WindowState dialog = addWindow(TYPE_APPLICATION, "dialog"); @@ -205,9 +207,8 @@ public class InsetsPolicyTest extends WindowTestsBase { // Assume mFocusedWindow is updated but mTopFullscreenOpaqueWindowState hasn't. final WindowState newFocusedFullscreenApp = addWindow(TYPE_APPLICATION, "newFullscreenApp"); - final InsetsVisibilities newRequestedVisibilities = new InsetsVisibilities(); - newRequestedVisibilities.setVisibility(ITYPE_STATUS_BAR, true); - newFocusedFullscreenApp.setRequestedVisibilities(newRequestedVisibilities); + newFocusedFullscreenApp.setRequestedVisibleTypes( + WindowInsets.Type.statusBars(), WindowInsets.Type.statusBars()); // Make sure status bar is hidden by previous insets state. mDisplayContent.getInsetsPolicy().updateBarControlTarget(fullscreenApp); @@ -261,17 +262,15 @@ public class InsetsPolicyTest extends WindowTestsBase { final WindowState statusBar = addStatusBar(); statusBar.setHasSurface(true); statusBar.getControllableInsetProvider().setServerVisible(true); - final WindowState navBar = addNonFocusableWindow(TYPE_NAVIGATION_BAR, "navBar"); + final WindowState navBar = addNavigationBar(); navBar.setHasSurface(true); navBar.getControllableInsetProvider().setServerVisible(true); final InsetsPolicy policy = spy(mDisplayContent.getInsetsPolicy()); doNothing().when(policy).startAnimation(anyBoolean(), any()); // Make both system bars invisible. - final InsetsVisibilities requestedVisibilities = new InsetsVisibilities(); - requestedVisibilities.setVisibility(ITYPE_STATUS_BAR, false); - requestedVisibilities.setVisibility(ITYPE_NAVIGATION_BAR, false); - mAppWindow.setRequestedVisibilities(requestedVisibilities); + mAppWindow.setRequestedVisibleTypes( + 0, navigationBars() | statusBars()); policy.updateBarControlTarget(mAppWindow); waitUntilWindowAnimatorIdle(); assertFalse(mDisplayContent.getInsetsStateController().getRawInsetsState() @@ -301,8 +300,7 @@ public class InsetsPolicyTest extends WindowTestsBase { @Test public void testShowTransientBars_statusBarCanBeTransient_appGetsStatusBarFakeControl() { addStatusBar().getControllableInsetProvider().getSource().setVisible(false); - addNonFocusableWindow(TYPE_NAVIGATION_BAR, "navBar") - .getControllableInsetProvider().setServerVisible(true); + addNavigationBar().getControllableInsetProvider().setServerVisible(true); final InsetsPolicy policy = spy(mDisplayContent.getInsetsPolicy()); doNothing().when(policy).startAnimation(anyBoolean(), any()); @@ -331,8 +329,8 @@ public class InsetsPolicyTest extends WindowTestsBase { public void testAbortTransientBars_bothCanBeAborted_appGetsBothRealControls() { final InsetsSource statusBarSource = addStatusBar().getControllableInsetProvider().getSource(); - final InsetsSource navBarSource = addNonFocusableWindow(TYPE_NAVIGATION_BAR, "navBar") - .getControllableInsetProvider().getSource(); + final InsetsSource navBarSource = + addNavigationBar().getControllableInsetProvider().getSource(); statusBarSource.setVisible(false); navBarSource.setVisible(false); mAppWindow.mAboveInsetsState.addSource(navBarSource); @@ -364,10 +362,8 @@ public class InsetsPolicyTest extends WindowTestsBase { assertTrue(state.getSource(ITYPE_STATUS_BAR).isVisible()); assertTrue(state.getSource(ITYPE_NAVIGATION_BAR).isVisible()); - final InsetsVisibilities requestedVisibilities = new InsetsVisibilities(); - requestedVisibilities.setVisibility(ITYPE_STATUS_BAR, true); - requestedVisibilities.setVisibility(ITYPE_NAVIGATION_BAR, true); - mAppWindow.setRequestedVisibilities(requestedVisibilities); + mAppWindow.setRequestedVisibleTypes( + navigationBars() | statusBars(), navigationBars() | statusBars()); policy.onInsetsModified(mAppWindow); waitUntilWindowAnimatorIdle(); @@ -383,8 +379,7 @@ public class InsetsPolicyTest extends WindowTestsBase { @Test public void testShowTransientBars_abortsWhenControlTargetChanges() { addStatusBar().getControllableInsetProvider().getSource().setVisible(false); - addNonFocusableWindow(TYPE_NAVIGATION_BAR, "navBar") - .getControllableInsetProvider().getSource().setVisible(false); + addNavigationBar().getControllableInsetProvider().getSource().setVisible(false); final WindowState app = addWindow(TYPE_APPLICATION, "app"); final WindowState app2 = addWindow(TYPE_APPLICATION, "app"); @@ -400,9 +395,15 @@ public class InsetsPolicyTest extends WindowTestsBase { assertFalse(policy.isTransient(ITYPE_NAVIGATION_BAR)); } - private WindowState addNonFocusableWindow(int type, String name) { - WindowState win = addWindow(type, name); + private WindowState addNavigationBar() { + final WindowState win = createWindow(null, TYPE_NAVIGATION_BAR, "navBar"); win.mAttrs.flags |= FLAG_NOT_FOCUSABLE; + win.mAttrs.providedInsets = new InsetsFrameProvider[] { + new InsetsFrameProvider(ITYPE_NAVIGATION_BAR), + new InsetsFrameProvider(ITYPE_BOTTOM_MANDATORY_GESTURES), + new InsetsFrameProvider(ITYPE_BOTTOM_TAPPABLE_ELEMENT) + }; + mDisplayContent.getDisplayPolicy().addWindowLw(win, win.mAttrs); return win; } @@ -429,6 +430,10 @@ public class InsetsPolicyTest extends WindowTestsBase { } private InsetsSourceControl[] addWindowAndGetControlsForDispatch(WindowState win) { + mDisplayContent.getDisplayPolicy().addWindowLw(win, win.mAttrs); + // Force update the focus in DisplayPolicy here. Otherwise, without server side focus + // update, the policy relying on windowing type will never get updated. + mDisplayContent.getDisplayPolicy().focusChangedLw(null, win); mDisplayContent.getInsetsPolicy().updateBarControlTarget(win); return mDisplayContent.getInsetsStateController().getControlsForDispatch(win); } diff --git a/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java index fe14d8e02592..c898119ea991 100644 --- a/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java @@ -24,6 +24,7 @@ import static android.view.InsetsState.ITYPE_EXTRA_NAVIGATION_BAR; import static android.view.InsetsState.ITYPE_IME; import static android.view.InsetsState.ITYPE_NAVIGATION_BAR; import static android.view.InsetsState.ITYPE_STATUS_BAR; +import static android.view.WindowInsets.Type.ime; import static android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; @@ -48,7 +49,6 @@ import android.platform.test.annotations.Presubmit; import android.util.SparseArray; import android.view.InsetsSourceControl; import android.view.InsetsState; -import android.view.InsetsVisibilities; import androidx.test.filters.SmallTest; @@ -187,10 +187,7 @@ public class InsetsStateControllerTest extends WindowTestsBase { getController().getSourceProvider(ITYPE_IME).setWindowContainer(mImeWindow, null, null); getController().onImeControlTargetChanged( mDisplayContent.getImeInputTarget().getWindowState()); - final InsetsVisibilities requestedVisibilities = new InsetsVisibilities(); - requestedVisibilities.setVisibility(ITYPE_IME, true); - mDisplayContent.getImeInputTarget().getWindowState() - .setRequestedVisibilities(requestedVisibilities); + mDisplayContent.getImeInputTarget().getWindowState().setRequestedVisibleTypes(ime(), ime()); getController().onInsetsModified(mDisplayContent.getImeInputTarget().getWindowState()); // Send our spy window (app) into the system so that we can detect the invocation. diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationPersisterTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationPersisterTest.java new file mode 100644 index 000000000000..1246d1ee46e8 --- /dev/null +++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationPersisterTest.java @@ -0,0 +1,263 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm; + +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; + +import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT; +import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP; +import static com.android.server.wm.LetterboxConfigurationPersister.LETTERBOX_CONFIGURATION_FILENAME; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.platform.test.annotations.Presubmit; +import android.util.AtomicFile; + +import androidx.test.filters.SmallTest; + +import com.android.internal.R; +import com.android.internal.annotations.VisibleForTesting; + +import junit.framework.Assert; + +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; + +@SmallTest +@Presubmit +public class LetterboxConfigurationPersisterTest { + + private static final long TIMEOUT = 2000L; // 2 secs + + private LetterboxConfigurationPersister mLetterboxConfigurationPersister; + private Context mContext; + private PersisterQueue mPersisterQueue; + private QueueState mQueueState; + private PersisterQueue.Listener mQueueListener; + private File mConfigFolder; + + @Before + public void setUp() throws Exception { + mContext = getInstrumentation().getTargetContext(); + mConfigFolder = mContext.getFilesDir(); + mPersisterQueue = new PersisterQueue(); + mQueueState = new QueueState(); + mLetterboxConfigurationPersister = new LetterboxConfigurationPersister(mContext, + () -> mContext.getResources().getInteger( + R.integer.config_letterboxDefaultPositionForHorizontalReachability), + () -> mContext.getResources().getInteger( + R.integer.config_letterboxDefaultPositionForVerticalReachability), + mConfigFolder, mPersisterQueue, mQueueState); + mQueueListener = queueEmpty -> mQueueState.onItemAdded(); + mPersisterQueue.addListener(mQueueListener); + mLetterboxConfigurationPersister.start(); + } + + public void tearDown() throws InterruptedException { + deleteConfiguration(mLetterboxConfigurationPersister, mPersisterQueue); + waitForCompletion(mPersisterQueue); + mPersisterQueue.removeListener(mQueueListener); + stopPersisterSafe(mPersisterQueue); + } + + @Test + public void test_whenStoreIsCreated_valuesAreDefaults() { + final int positionForHorizontalReachability = + mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability(); + final int defaultPositionForHorizontalReachability = + mContext.getResources().getInteger( + R.integer.config_letterboxDefaultPositionForHorizontalReachability); + Assert.assertEquals(defaultPositionForHorizontalReachability, + positionForHorizontalReachability); + final int positionForVerticalReachability = + mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability(); + final int defaultPositionForVerticalReachability = + mContext.getResources().getInteger( + R.integer.config_letterboxDefaultPositionForVerticalReachability); + Assert.assertEquals(defaultPositionForVerticalReachability, + positionForVerticalReachability); + } + + @Test + public void test_whenUpdatedWithNewValues_valuesAreWritten() { + mLetterboxConfigurationPersister.setLetterboxPositionForHorizontalReachability( + LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT); + mLetterboxConfigurationPersister.setLetterboxPositionForVerticalReachability( + LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP); + waitForCompletion(mPersisterQueue); + final int newPositionForHorizontalReachability = + mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability(); + final int newPositionForVerticalReachability = + mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability(); + Assert.assertEquals(LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT, + newPositionForHorizontalReachability); + Assert.assertEquals(LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP, + newPositionForVerticalReachability); + } + + @Test + public void test_whenUpdatedWithNewValues_valuesAreReadAfterRestart() { + final PersisterQueue firstPersisterQueue = new PersisterQueue(); + final LetterboxConfigurationPersister firstPersister = new LetterboxConfigurationPersister( + mContext, () -> -1, () -> -1, mContext.getFilesDir(), firstPersisterQueue, + mQueueState); + firstPersister.start(); + firstPersister.setLetterboxPositionForHorizontalReachability( + LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT); + firstPersister.setLetterboxPositionForVerticalReachability( + LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP); + waitForCompletion(firstPersisterQueue); + stopPersisterSafe(firstPersisterQueue); + final PersisterQueue secondPersisterQueue = new PersisterQueue(); + final LetterboxConfigurationPersister secondPersister = new LetterboxConfigurationPersister( + mContext, () -> -1, () -> -1, mContext.getFilesDir(), secondPersisterQueue, + mQueueState); + secondPersister.start(); + final int newPositionForHorizontalReachability = + secondPersister.getLetterboxPositionForHorizontalReachability(); + final int newPositionForVerticalReachability = + secondPersister.getLetterboxPositionForVerticalReachability(); + Assert.assertEquals(LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT, + newPositionForHorizontalReachability); + Assert.assertEquals(LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP, + newPositionForVerticalReachability); + deleteConfiguration(secondPersister, secondPersisterQueue); + waitForCompletion(secondPersisterQueue); + stopPersisterSafe(secondPersisterQueue); + } + + @Test + public void test_whenUpdatedWithNewValuesAndDeleted_valuesAreDefaults() { + mLetterboxConfigurationPersister.setLetterboxPositionForHorizontalReachability( + LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT); + mLetterboxConfigurationPersister.setLetterboxPositionForVerticalReachability( + LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP); + waitForCompletion(mPersisterQueue); + final int newPositionForHorizontalReachability = + mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability(); + final int newPositionForVerticalReachability = + mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability(); + Assert.assertEquals(LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT, + newPositionForHorizontalReachability); + Assert.assertEquals(LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP, + newPositionForVerticalReachability); + deleteConfiguration(mLetterboxConfigurationPersister, mPersisterQueue); + waitForCompletion(mPersisterQueue); + final int positionForHorizontalReachability = + mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability(); + final int defaultPositionForHorizontalReachability = + mContext.getResources().getInteger( + R.integer.config_letterboxDefaultPositionForHorizontalReachability); + Assert.assertEquals(defaultPositionForHorizontalReachability, + positionForHorizontalReachability); + final int positionForVerticalReachability = + mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability(); + final int defaultPositionForVerticalReachability = + mContext.getResources().getInteger( + R.integer.config_letterboxDefaultPositionForVerticalReachability); + Assert.assertEquals(defaultPositionForVerticalReachability, + positionForVerticalReachability); + } + + private void stopPersisterSafe(PersisterQueue persisterQueue) { + try { + persisterQueue.stopPersisting(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + private void waitForCompletion(PersisterQueue persisterQueue) { + final long endTime = System.currentTimeMillis() + TIMEOUT; + // The queue could be empty but the last item still processing and not completed. For this + // reason the completion happens when there are not more items to process and the last one + // has completed. + while (System.currentTimeMillis() < endTime && (!isQueueEmpty(persisterQueue) + || !hasLastItemCompleted())) { + try { + Thread.sleep(100); + } catch (InterruptedException ie) { /* Nope */} + } + } + + private boolean isQueueEmpty(PersisterQueue persisterQueue) { + return persisterQueue.findLastItem( + writeQueueItem -> true, PersisterQueue.WriteQueueItem.class) != null; + } + + private boolean hasLastItemCompleted() { + return mQueueState.isEmpty(); + } + + private void deleteConfiguration(LetterboxConfigurationPersister persister, + PersisterQueue persisterQueue) { + final AtomicFile fileToDelete = new AtomicFile( + new File(mConfigFolder, LETTERBOX_CONFIGURATION_FILENAME)); + persisterQueue.addItem( + new DeleteFileCommand(fileToDelete, mQueueState.andThen( + s -> persister.useDefaultValue())), true); + } + + private static class DeleteFileCommand implements + PersisterQueue.WriteQueueItem<DeleteFileCommand> { + + @NonNull + private final AtomicFile mFileToDelete; + @Nullable + private final Consumer<String> mOnComplete; + + DeleteFileCommand(@NonNull AtomicFile fileToDelete, Consumer<String> onComplete) { + mFileToDelete = fileToDelete; + mOnComplete = onComplete; + } + + @Override + public void process() { + mFileToDelete.delete(); + if (mOnComplete != null) { + mOnComplete.accept("DeleteFileCommand"); + } + } + } + + // Contains the current length of the persister queue + private static class QueueState implements Consumer<String> { + + // The current number of commands in the queue + @VisibleForTesting + private final AtomicInteger mCounter = new AtomicInteger(0); + + @Override + public void accept(String s) { + mCounter.decrementAndGet(); + } + + void onItemAdded() { + mCounter.incrementAndGet(); + } + + boolean isEmpty() { + return mCounter.get() == 0; + } + + } +} diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationTest.java new file mode 100644 index 000000000000..c927f9e449d5 --- /dev/null +++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationTest.java @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm; + +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; + +import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER; +import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT; +import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT; +import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM; +import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER; +import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.platform.test.annotations.Presubmit; + +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Test; + +import java.util.function.Consumer; + +@SmallTest +@Presubmit +public class LetterboxConfigurationTest { + + private LetterboxConfiguration mLetterboxConfiguration; + private LetterboxConfigurationPersister mLetterboxConfigurationPersister; + + @Before + public void setUp() throws Exception { + Context context = getInstrumentation().getTargetContext(); + mLetterboxConfigurationPersister = mock(LetterboxConfigurationPersister.class); + mLetterboxConfiguration = new LetterboxConfiguration(context, + mLetterboxConfigurationPersister); + } + + @Test + public void test_whenReadingValues_storeIsInvoked() { + mLetterboxConfiguration.getLetterboxPositionForHorizontalReachability(); + verify(mLetterboxConfigurationPersister).getLetterboxPositionForHorizontalReachability(); + mLetterboxConfiguration.getLetterboxPositionForVerticalReachability(); + verify(mLetterboxConfigurationPersister).getLetterboxPositionForVerticalReachability(); + } + + @Test + public void test_whenSettingValues_updateConfigurationIsInvoked() { + mLetterboxConfiguration.movePositionForHorizontalReachabilityToNextRightStop(); + verify(mLetterboxConfigurationPersister).setLetterboxPositionForHorizontalReachability( + anyInt()); + mLetterboxConfiguration.movePositionForVerticalReachabilityToNextBottomStop(); + verify(mLetterboxConfigurationPersister).setLetterboxPositionForVerticalReachability( + anyInt()); + } + + @Test + public void test_whenMovedHorizontally_updatePositionAccordingly() { + // Starting from center + assertForHorizontalMove( + /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER, + /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT, + /* expectedTime */ 1, + LetterboxConfiguration::movePositionForHorizontalReachabilityToNextLeftStop); + assertForHorizontalMove( + /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER, + /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT, + /* expectedTime */ 1, + LetterboxConfiguration::movePositionForHorizontalReachabilityToNextRightStop); + // Starting from left + assertForHorizontalMove( + /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT, + /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT, + /* expectedTime */ 2, + LetterboxConfiguration::movePositionForHorizontalReachabilityToNextLeftStop); + assertForHorizontalMove( + /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT, + /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER, + /* expectedTime */ 1, + LetterboxConfiguration::movePositionForHorizontalReachabilityToNextRightStop); + // Starting from right + assertForHorizontalMove( + /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT, + /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT, + /* expectedTime */ 2, + LetterboxConfiguration::movePositionForHorizontalReachabilityToNextRightStop); + assertForHorizontalMove( + /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT, + /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER, + /* expectedTime */ 2, + LetterboxConfiguration::movePositionForHorizontalReachabilityToNextLeftStop); + } + + @Test + public void test_whenMovedVertically_updatePositionAccordingly() { + // Starting from center + assertForVerticalMove( + /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER, + /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM, + /* expectedTime */ 1, + LetterboxConfiguration::movePositionForVerticalReachabilityToNextBottomStop); + assertForVerticalMove( + /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER, + /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP, + /* expectedTime */ 1, + LetterboxConfiguration::movePositionForVerticalReachabilityToNextTopStop); + // Starting from top + assertForVerticalMove( + /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP, + /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER, + /* expectedTime */ 1, + LetterboxConfiguration::movePositionForVerticalReachabilityToNextBottomStop); + assertForVerticalMove( + /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP, + /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP, + /* expectedTime */ 2, + LetterboxConfiguration::movePositionForVerticalReachabilityToNextTopStop); + // Starting from bottom + assertForVerticalMove( + /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM, + /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER, + /* expectedTime */ 2, + LetterboxConfiguration::movePositionForVerticalReachabilityToNextTopStop); + assertForVerticalMove( + /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM, + /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM, + /* expectedTime */ 2, + LetterboxConfiguration::movePositionForVerticalReachabilityToNextBottomStop); + } + + private void assertForHorizontalMove(int from, int expected, int expectedTime, + Consumer<LetterboxConfiguration> move) { + // We are in the current position + when(mLetterboxConfiguration.getLetterboxPositionForHorizontalReachability()) + .thenReturn(from); + move.accept(mLetterboxConfiguration); + verify(mLetterboxConfigurationPersister, + times(expectedTime)).setLetterboxPositionForHorizontalReachability( + expected); + } + + private void assertForVerticalMove(int from, int expected, int expectedTime, + Consumer<LetterboxConfiguration> move) { + // We are in the current position + when(mLetterboxConfiguration.getLetterboxPositionForVerticalReachability()) + .thenReturn(from); + move.accept(mLetterboxConfiguration); + verify(mLetterboxConfigurationPersister, + times(expectedTime)).setLetterboxPositionForVerticalReachability( + expected); + } +} diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowContainerInsetsSourceProviderTest.java b/services/tests/wmtests/src/com/android/server/wm/WindowContainerInsetsSourceProviderTest.java index e824f3d2916d..383722a556bb 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowContainerInsetsSourceProviderTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowContainerInsetsSourceProviderTest.java @@ -18,6 +18,7 @@ package com.android.server.wm; import static android.view.InsetsState.ITYPE_IME; import static android.view.InsetsState.ITYPE_STATUS_BAR; +import static android.view.WindowInsets.Type.statusBars; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD; @@ -31,7 +32,6 @@ import android.graphics.Insets; import android.graphics.Rect; import android.platform.test.annotations.Presubmit; import android.view.InsetsSource; -import android.view.InsetsVisibilities; import androidx.test.filters.SmallTest; @@ -207,9 +207,7 @@ public class WindowContainerInsetsSourceProviderTest extends WindowTestsBase { statusBar.getFrame().set(0, 0, 500, 100); mProvider.setWindowContainer(statusBar, null, null); mProvider.updateControlForTarget(target, false /* force */); - final InsetsVisibilities requestedVisibilities = new InsetsVisibilities(); - requestedVisibilities.setVisibility(ITYPE_STATUS_BAR, false); - target.setRequestedVisibilities(requestedVisibilities); + target.setRequestedVisibleTypes(0, statusBars()); mProvider.updateClientVisibility(target); assertFalse(mSource.isVisible()); } @@ -220,9 +218,7 @@ public class WindowContainerInsetsSourceProviderTest extends WindowTestsBase { final WindowState target = createWindow(null, TYPE_APPLICATION, "target"); statusBar.getFrame().set(0, 0, 500, 100); mProvider.setWindowContainer(statusBar, null, null); - final InsetsVisibilities requestedVisibilities = new InsetsVisibilities(); - requestedVisibilities.setVisibility(ITYPE_STATUS_BAR, false); - target.setRequestedVisibilities(requestedVisibilities); + target.setRequestedVisibleTypes(0, statusBars()); mProvider.updateClientVisibility(target); assertTrue(mSource.isVisible()); } diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowLayoutTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowLayoutTests.java index 739e783e7c1b..56c59cc8eca9 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowLayoutTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowLayoutTests.java @@ -41,7 +41,6 @@ import android.platform.test.annotations.Presubmit; import android.view.DisplayCutout; import android.view.Gravity; import android.view.InsetsState; -import android.view.InsetsVisibilities; import android.view.WindowInsets; import android.view.WindowLayout; import android.view.WindowManager; @@ -81,7 +80,7 @@ public class WindowLayoutTests { private int mWindowingMode; private int mRequestedWidth; private int mRequestedHeight; - private InsetsVisibilities mRequestedVisibilities; + private int mRequestedVisibleTypes; private float mCompatScale; @Before @@ -98,14 +97,14 @@ public class WindowLayoutTests { mWindowingMode = WindowConfiguration.WINDOWING_MODE_FULLSCREEN; mRequestedWidth = DISPLAY_WIDTH; mRequestedHeight = DISPLAY_HEIGHT; - mRequestedVisibilities = new InsetsVisibilities(); + mRequestedVisibleTypes = WindowInsets.Type.defaultVisible(); mCompatScale = 1f; mFrames.attachedFrame = null; } private void computeFrames() { mWindowLayout.computeFrames(mAttrs, mState, mDisplayCutoutSafe, mWindowBounds, - mWindowingMode, mRequestedWidth, mRequestedHeight, mRequestedVisibilities, + mWindowingMode, mRequestedWidth, mRequestedHeight, mRequestedVisibleTypes, mCompatScale, mFrames); } diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java index cf24ff2e99a5..4429aef02f94 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java @@ -71,10 +71,10 @@ import android.util.MergedConfiguration; import android.view.IWindowSessionCallback; import android.view.InsetsSourceControl; import android.view.InsetsState; -import android.view.InsetsVisibilities; import android.view.Surface; import android.view.SurfaceControl; import android.view.View; +import android.view.WindowInsets; import android.view.WindowManager; import android.window.ClientWindowFrames; import android.window.ScreenCapture; @@ -338,7 +338,7 @@ public class WindowManagerServiceTests extends WindowTestsBase { .getWindowType(eq(windowContextToken)); mWm.addWindow(session, new TestIWindow(), params, View.VISIBLE, DEFAULT_DISPLAY, - UserHandle.USER_SYSTEM, new InsetsVisibilities(), null, new InsetsState(), + UserHandle.USER_SYSTEM, WindowInsets.Type.defaultVisible(), null, new InsetsState(), new InsetsSourceControl[0], new Rect(), new float[1]); verify(mWm.mWindowContextListenerController, never()).registerWindowContainerListener(any(), diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java index 163666792bc7..0139f6a5695a 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java @@ -26,6 +26,7 @@ import static android.view.InsetsState.ITYPE_STATUS_BAR; import static android.view.Surface.ROTATION_0; import static android.view.Surface.ROTATION_270; import static android.view.Surface.ROTATION_90; +import static android.view.WindowInsets.Type.statusBars; import static android.view.WindowManager.LayoutParams.FIRST_SUB_WINDOW; import static android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; @@ -92,7 +93,6 @@ import android.view.Gravity; import android.view.InputWindowHandle; import android.view.InsetsSource; import android.view.InsetsState; -import android.view.InsetsVisibilities; import android.view.SurfaceControl; import android.view.WindowManager; @@ -430,9 +430,7 @@ public class WindowStateTests extends WindowTestsBase { null /* imeFrameProvider */); mDisplayContent.getInsetsStateController().onBarControlTargetChanged( app, null /* fakeTopControlling */, app, null /* fakeNavControlling */); - final InsetsVisibilities requestedVisibilities = new InsetsVisibilities(); - requestedVisibilities.setVisibility(ITYPE_STATUS_BAR, false); - app.setRequestedVisibilities(requestedVisibilities); + app.setRequestedVisibleTypes(0, statusBars()); mDisplayContent.getInsetsStateController().getSourceProvider(ITYPE_STATUS_BAR) .updateClientVisibility(app); waitUntilHandlersIdle(); diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java index 40326e9ad7f6..eca7cbbc5486 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java @@ -26,6 +26,8 @@ import static android.content.pm.ActivityInfo.RESIZE_MODE_RESIZEABLE; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; import static android.os.Process.SYSTEM_UID; +import static android.view.InsetsState.ITYPE_BOTTOM_MANDATORY_GESTURES; +import static android.view.InsetsState.ITYPE_BOTTOM_TAPPABLE_ELEMENT; import static android.view.InsetsState.ITYPE_NAVIGATION_BAR; import static android.view.InsetsState.ITYPE_STATUS_BAR; import static android.view.InsetsState.ITYPE_TOP_MANDATORY_GESTURES; @@ -92,7 +94,6 @@ import android.view.IWindow; import android.view.InsetsFrameProvider; import android.view.InsetsSourceControl; import android.view.InsetsState; -import android.view.InsetsVisibilities; import android.view.Surface; import android.view.SurfaceControl; import android.view.SurfaceControl.Transaction; @@ -347,6 +348,11 @@ class WindowTestsBase extends SystemServiceTestsBase { LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; mNavBarWindow.mAttrs.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_LAYOUT_SIZE_EXTENDED_BY_CUTOUT; + mNavBarWindow.mAttrs.providedInsets = new InsetsFrameProvider[] { + new InsetsFrameProvider(ITYPE_NAVIGATION_BAR), + new InsetsFrameProvider(ITYPE_BOTTOM_MANDATORY_GESTURES), + new InsetsFrameProvider(ITYPE_BOTTOM_TAPPABLE_ELEMENT) + }; for (int rot = Surface.ROTATION_0; rot <= Surface.ROTATION_270; rot++) { mNavBarWindow.mAttrs.paramsForRotation[rot] = getNavBarLayoutParamsForRotation(rot); @@ -400,6 +406,11 @@ class WindowTestsBase extends SystemServiceTestsBase { lp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_LAYOUT_SIZE_EXTENDED_BY_CUTOUT; lp.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; + lp.providedInsets = new InsetsFrameProvider[] { + new InsetsFrameProvider(ITYPE_NAVIGATION_BAR), + new InsetsFrameProvider(ITYPE_BOTTOM_MANDATORY_GESTURES), + new InsetsFrameProvider(ITYPE_BOTTOM_TAPPABLE_ELEMENT) + }; return lp; } @@ -846,7 +857,7 @@ class WindowTestsBase extends SystemServiceTestsBase { @Override public void topFocusedWindowChanged(ComponentName component, - InsetsVisibilities requestedVisibilities) { + int requestedVisibleTypes) { } }; } diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java index 921f6e20c3ad..372fdaf08bf9 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java @@ -129,7 +129,10 @@ final class HotwordDetectionConnection { private static final String KEY_RESTART_PERIOD_IN_SECONDS = "restart_period_in_seconds"; // TODO: These constants need to be refined. - private static final long VALIDATION_TIMEOUT_MILLIS = 4000; + // The validation timeout value is 3 seconds for onDetect of DSP trigger event. + private static final long VALIDATION_TIMEOUT_MILLIS = 3000; + // Write the onDetect timeout metric when it takes more time than MAX_VALIDATION_TIMEOUT_MILLIS. + private static final long MAX_VALIDATION_TIMEOUT_MILLIS = 4000; private static final long MAX_UPDATE_TIMEOUT_MILLIS = 30000; private static final long EXTERNAL_HOTWORD_CLEANUP_MILLIS = 2000; private static final Duration MAX_UPDATE_TIMEOUT_DURATION = @@ -659,6 +662,10 @@ final class HotwordDetectionConnection { synchronized (mLock) { mValidatingDspTrigger = true; mRemoteHotwordDetectionService.run(service -> { + // We use the VALIDATION_TIMEOUT_MILLIS to inform that the client needs to invoke + // the callback before timeout value. In order to reduce the latency impact between + // server side and client side, we need to use another timeout value + // MAX_VALIDATION_TIMEOUT_MILLIS to monitor it. mCancellationKeyPhraseDetectionFuture = mScheduledExecutorService.schedule( () -> { // TODO: avoid allocate every time @@ -673,7 +680,7 @@ final class HotwordDetectionConnection { Slog.w(TAG, "Failed to report onError status: ", e); } }, - VALIDATION_TIMEOUT_MILLIS, + MAX_VALIDATION_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); service.detectFromDspSource( recognitionEvent, diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java index 439eaa69e771..ef693b5278a0 100644 --- a/telephony/java/android/telephony/SubscriptionManager.java +++ b/telephony/java/android/telephony/SubscriptionManager.java @@ -3942,6 +3942,10 @@ public class SubscriptionManager { * may provide one. Or, a carrier may decide to provide the phone number via source * {@link #PHONE_NUMBER_SOURCE_CARRIER carrier} if neither source UICC nor IMS is available. * + * <p>The availability and correctness of the phone number depends on the underlying source + * and the network etc. Additional verification is needed to use this number for + * security-related or other sensitive scenarios. + * * @param subscriptionId the subscription ID, or {@link #DEFAULT_SUBSCRIPTION_ID} * for the default one. * @param source the source of the phone number, one of the PHONE_NUMBER_SOURCE_* constants. @@ -4175,18 +4179,18 @@ public class SubscriptionManager { */ @SystemApi @RequiresPermission(Manifest.permission.MANAGE_SUBSCRIPTION_USER_ASSOCIATION) - public void setUserHandle(int subscriptionId, @Nullable UserHandle userHandle) { + public void setSubscriptionUserHandle(int subscriptionId, @Nullable UserHandle userHandle) { if (!isValidSubscriptionId(subscriptionId)) { - throw new IllegalArgumentException("[setUserHandle]: Invalid subscriptionId: " - + subscriptionId); + throw new IllegalArgumentException("[setSubscriptionUserHandle]: " + + "Invalid subscriptionId: " + subscriptionId); } try { ISub iSub = TelephonyManager.getSubscriptionService(); if (iSub != null) { - iSub.setUserHandle(userHandle, subscriptionId, mContext.getOpPackageName()); + iSub.setSubscriptionUserHandle(userHandle, subscriptionId); } else { - throw new IllegalStateException("[setUserHandle]: " + throw new IllegalStateException("[setSubscriptionUserHandle]: " + "subscription service unavailable"); } } catch (RemoteException ex) { @@ -4211,18 +4215,18 @@ public class SubscriptionManager { */ @SystemApi @RequiresPermission(Manifest.permission.MANAGE_SUBSCRIPTION_USER_ASSOCIATION) - public @Nullable UserHandle getUserHandle(int subscriptionId) { + public @Nullable UserHandle getSubscriptionUserHandle(int subscriptionId) { if (!isValidSubscriptionId(subscriptionId)) { - throw new IllegalArgumentException("[getUserHandle]: Invalid subscriptionId: " - + subscriptionId); + throw new IllegalArgumentException("[getSubscriptionUserHandle]: " + + "Invalid subscriptionId: " + subscriptionId); } try { ISub iSub = TelephonyManager.getSubscriptionService(); if (iSub != null) { - return iSub.getUserHandle(subscriptionId, mContext.getOpPackageName()); + return iSub.getSubscriptionUserHandle(subscriptionId); } else { - throw new IllegalStateException("[getUserHandle]: " + throw new IllegalStateException("[getSubscriptionUserHandle]: " + "subscription service unavailable"); } } catch (RemoteException ex) { diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java index 9ecebf1a28f3..ff1b1c097d68 100644 --- a/telephony/java/android/telephony/TelephonyManager.java +++ b/telephony/java/android/telephony/TelephonyManager.java @@ -3323,15 +3323,14 @@ public class TelephonyManager { case NETWORK_TYPE_TD_SCDMA: return NETWORK_TYPE_BITMASK_TD_SCDMA; case NETWORK_TYPE_LTE: - return NETWORK_TYPE_BITMASK_LTE; case NETWORK_TYPE_LTE_CA: - return NETWORK_TYPE_BITMASK_LTE_CA; + return NETWORK_TYPE_BITMASK_LTE; case NETWORK_TYPE_NR: return NETWORK_TYPE_BITMASK_NR; case NETWORK_TYPE_IWLAN: return NETWORK_TYPE_BITMASK_IWLAN; case NETWORK_TYPE_IDEN: - return (1 << (NETWORK_TYPE_IDEN - 1)); + return NETWORK_TYPE_BITMASK_IDEN; default: return NETWORK_TYPE_BITMASK_UNKNOWN; } @@ -13911,7 +13910,8 @@ public class TelephonyManager { NETWORK_TYPE_BITMASK_LTE, NETWORK_TYPE_BITMASK_LTE_CA, NETWORK_TYPE_BITMASK_NR, - NETWORK_TYPE_BITMASK_IWLAN + NETWORK_TYPE_BITMASK_IWLAN, + NETWORK_TYPE_BITMASK_IDEN }) public @interface NetworkTypeBitMask {} @@ -13971,6 +13971,10 @@ public class TelephonyManager { */ public static final long NETWORK_TYPE_BITMASK_HSPA = (1 << (NETWORK_TYPE_HSPA -1)); /** + * network type bitmask indicating the support of radio tech iDen. + */ + public static final long NETWORK_TYPE_BITMASK_IDEN = (1 << (NETWORK_TYPE_IDEN - 1)); + /** * network type bitmask indicating the support of radio tech HSPAP. */ public static final long NETWORK_TYPE_BITMASK_HSPAP = (1 << (NETWORK_TYPE_HSPAP -1)); @@ -13988,12 +13992,13 @@ public class TelephonyManager { */ public static final long NETWORK_TYPE_BITMASK_LTE = (1 << (NETWORK_TYPE_LTE -1)); /** - * NOT USED; this bitmask is exposed accidentally, will be deprecated in U. + * NOT USED; this bitmask is exposed accidentally. * If used, will be converted to {@link #NETWORK_TYPE_BITMASK_LTE}. * network type bitmask indicating the support of radio tech LTE CA (carrier aggregation). * - * @see #NETWORK_TYPE_BITMASK_LTE + * @deprecated Please use {@link #NETWORK_TYPE_BITMASK_LTE} instead. */ + @Deprecated public static final long NETWORK_TYPE_BITMASK_LTE_CA = (1 << (NETWORK_TYPE_LTE_CA -1)); /** diff --git a/telephony/java/android/telephony/ims/ImsService.java b/telephony/java/android/telephony/ims/ImsService.java index f0401534fac4..e8642fef04f5 100644 --- a/telephony/java/android/telephony/ims/ImsService.java +++ b/telephony/java/android/telephony/ims/ImsService.java @@ -204,6 +204,7 @@ public class ImsService extends Service { private IImsServiceControllerListener mListener; private final Object mListenerLock = new Object(); + private final Object mExecutorLock = new Object(); private Executor mExecutor; /** @@ -214,10 +215,6 @@ public class ImsService extends Service { * vendor use Runnable::run. */ public ImsService() { - mExecutor = ImsService.this.getExecutor(); - if (mExecutor == null) { - mExecutor = Runnable::run; - } } /** @@ -356,7 +353,7 @@ public class ImsService extends Service { ImsConfigImplBase c = ImsService.this.getConfigForSubscription(slotId, subId); if (c != null) { - c.setDefaultExecutor(mExecutor); + c.setDefaultExecutor(getCachedExecutor()); return c.getIImsConfig(); } else { return null; @@ -370,7 +367,7 @@ public class ImsService extends Service { ImsRegistrationImplBase r = ImsService.this.getRegistrationForSubscription(slotId, subId); if (r != null) { - r.setDefaultExecutor(mExecutor); + r.setDefaultExecutor(getCachedExecutor()); return r.getBinder(); } else { return null; @@ -383,7 +380,7 @@ public class ImsService extends Service { return executeMethodAsyncForResult(() -> { SipTransportImplBase s = ImsService.this.getSipTransport(slotId); if (s != null) { - s.setDefaultExecutor(mExecutor); + s.setDefaultExecutor(getCachedExecutor()); return s.getBinder(); } else { return null; @@ -427,11 +424,21 @@ public class ImsService extends Service { return null; } + private Executor getCachedExecutor() { + synchronized (mExecutorLock) { + if (mExecutor == null) { + Executor e = ImsService.this.getExecutor(); + mExecutor = (e != null) ? e : Runnable::run; + } + return mExecutor; + } + } + private IImsMmTelFeature createMmTelFeatureInternal(int slotId, int subscriptionId) { MmTelFeature f = createMmTelFeatureForSubscription(slotId, subscriptionId); if (f != null) { setupFeature(f, slotId, ImsFeature.FEATURE_MMTEL); - f.setDefaultExecutor(mExecutor); + f.setDefaultExecutor(getCachedExecutor()); return f.getBinder(); } else { Log.e(LOG_TAG, "createMmTelFeatureInternal: null feature returned."); @@ -443,7 +450,7 @@ public class ImsService extends Service { MmTelFeature f = createEmergencyOnlyMmTelFeature(slotId); if (f != null) { setupFeature(f, slotId, ImsFeature.FEATURE_MMTEL); - f.setDefaultExecutor(mExecutor); + f.setDefaultExecutor(getCachedExecutor()); return f.getBinder(); } else { Log.e(LOG_TAG, "createEmergencyOnlyMmTelFeatureInternal: null feature returned."); @@ -454,7 +461,7 @@ public class ImsService extends Service { private IImsRcsFeature createRcsFeatureInternal(int slotId, int subI) { RcsFeature f = createRcsFeatureForSubscription(slotId, subI); if (f != null) { - f.setDefaultExecutor(mExecutor); + f.setDefaultExecutor(getCachedExecutor()); setupFeature(f, slotId, ImsFeature.FEATURE_RCS); return f.getBinder(); } else { @@ -609,7 +616,8 @@ public class ImsService extends Service { private void executeMethodAsync(Runnable r, String errorLogName) { try { CompletableFuture.runAsync( - () -> TelephonyUtils.runWithCleanCallingIdentity(r), mExecutor).join(); + () -> TelephonyUtils.runWithCleanCallingIdentity(r), + getCachedExecutor()).join(); } catch (CancellationException | CompletionException e) { Log.w(LOG_TAG, "ImsService Binder - " + errorLogName + " exception: " + e.getMessage()); @@ -618,7 +626,7 @@ public class ImsService extends Service { private <T> T executeMethodAsyncForResult(Supplier<T> r, String errorLogName) { CompletableFuture<T> future = CompletableFuture.supplyAsync( - () -> TelephonyUtils.runWithCleanCallingIdentity(r), mExecutor); + () -> TelephonyUtils.runWithCleanCallingIdentity(r), getCachedExecutor()); try { return future.get(); } catch (ExecutionException | InterruptedException e) { diff --git a/telephony/java/com/android/internal/telephony/ISub.aidl b/telephony/java/com/android/internal/telephony/ISub.aidl index 0211a7f5c5c5..4752cca8bd6c 100755 --- a/telephony/java/com/android/internal/telephony/ISub.aidl +++ b/telephony/java/com/android/internal/telephony/ISub.aidl @@ -323,22 +323,20 @@ interface ISub { * * @param userHandle the user handle for this subscription * @param subId the unique SubscriptionInfo index in database - * @param callingPackage The package making the IPC. * * @throws SecurityException if doesn't have MANAGE_SUBSCRIPTION_USER_ASSOCIATION * @throws IllegalArgumentException if subId is invalid. */ - int setUserHandle(in UserHandle userHandle, int subId, String callingPackage); + int setSubscriptionUserHandle(in UserHandle userHandle, int subId); /** * Get UserHandle for this subscription * * @param subId the unique SubscriptionInfo index in database - * @param callingPackage the package making the IPC * @return userHandle associated with this subscription. * - * @throws SecurityException if doesn't have SMANAGE_SUBSCRIPTION_USER_ASSOCIATION + * @throws SecurityException if doesn't have MANAGE_SUBSCRIPTION_USER_ASSOCIATION * @throws IllegalArgumentException if subId is invalid. */ - UserHandle getUserHandle(int subId, String callingPackage); + UserHandle getSubscriptionUserHandle(int subId); } diff --git a/telephony/java/com/android/internal/telephony/RILConstants.java b/telephony/java/com/android/internal/telephony/RILConstants.java index 98926710e053..9f612e6d7dd9 100644 --- a/telephony/java/com/android/internal/telephony/RILConstants.java +++ b/telephony/java/com/android/internal/telephony/RILConstants.java @@ -536,6 +536,12 @@ public interface RILConstants { int RIL_REQUEST_TRIGGER_EMERGENCY_NETWORK_SCAN = 230; int RIL_REQUEST_CANCEL_EMERGENCY_NETWORK_SCAN = 231; int RIL_REQUEST_EXIT_EMERGENCY_MODE = 232; + int RIL_REQUEST_SET_SRVCC_CALL_INFO = 233; + int RIL_REQUEST_UPDATE_IMS_REGISTRATION_INFO = 234; + int RIL_REQUEST_START_IMS_TRAFFIC = 235; + int RIL_REQUEST_STOP_IMS_TRAFFIC = 236; + int RIL_REQUEST_SEND_ANBR_QUERY = 237; + int RIL_REQUEST_TRIGGER_EPS_FALLBACK = 238; /* Responses begin */ int RIL_RESPONSE_ACKNOWLEDGEMENT = 800; @@ -607,4 +613,7 @@ public interface RILConstants { int RIL_UNSOL_REGISTRATION_FAILED = 1104; int RIL_UNSOL_BARRING_INFO_CHANGED = 1105; int RIL_UNSOL_EMERGENCY_NETWORK_SCAN_RESULT = 1106; + int RIL_UNSOL_TRIGGER_IMS_DEREGISTRATION = 1107; + int RIL_UNSOL_CONNECTION_SETUP_FAILURE = 1108; + int RIL_UNSOL_NOTIFY_ANBR = 1109; } diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeAppAutoFocusHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeAppAutoFocusHelper.kt index 16753e63eb01..ca5b2afd0917 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeAppAutoFocusHelper.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeAppAutoFocusHelper.kt @@ -24,6 +24,8 @@ import androidx.test.uiautomator.By import androidx.test.uiautomator.Until import com.android.server.wm.flicker.testapp.ActivityOptions import com.android.server.wm.traces.common.ComponentNameMatcher +import com.android.server.wm.traces.common.Condition +import com.android.server.wm.traces.common.DeviceStateDump import com.android.server.wm.traces.parser.toFlickerComponent import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper import java.util.regex.Pattern @@ -47,9 +49,10 @@ constructor( wmHelper: WindowManagerStateHelper, expectedWindowName: String, action: String?, - stringExtras: Map<String, String> + stringExtras: Map<String, String>, + waitConditions: Array<Condition<DeviceStateDump>> ) { - super.launchViaIntent(wmHelper, expectedWindowName, action, stringExtras) + super.launchViaIntent(wmHelper, expectedWindowName, action, stringExtras, waitConditions) waitIMEShown(wmHelper) } diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppAfterCameraTest_ShellTransit.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppAfterCameraTest_ShellTransit.kt index 0837c0046183..56869655be98 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppAfterCameraTest_ShellTransit.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppAfterCameraTest_ShellTransit.kt @@ -44,7 +44,7 @@ class OpenAppAfterCameraTest_ShellTransit(testSpec: FlickerTestParameter) : OpenAppAfterCameraTest(testSpec) { @Before override fun before() { - Assume.assumeFalse(isShellTransitionsEnabled) + Assume.assumeTrue(isShellTransitionsEnabled) } @FlakyTest |