diff options
297 files changed, 9478 insertions, 4650 deletions
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/core/api/current.txt b/core/api/current.txt index 40e0c5b74f8e..218d7bd14ceb 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"; @@ -19497,7 +19498,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; } @@ -19529,7 +19530,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 { @@ -19727,7 +19728,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 +44007,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 +47433,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 74288ae59d5e..a382ecfc99d3 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -9351,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); } @@ -13397,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); @@ -13407,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/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/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/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/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/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/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/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/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/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/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/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/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 5e836ef186d4..ff4588a7bc9b 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -4970,7 +4970,7 @@ public final class ViewRootImpl implements ViewParent, } void reportKeepClearAreasChanged() { - if (!mHasPendingKeepClearAreaChange) { + if (!mHasPendingKeepClearAreaChange || mView == null) { return; } mHasPendingKeepClearAreaChange = false; 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/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/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/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/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/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/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/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/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/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/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/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/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/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..d09aec9460dd 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,42 @@ 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 + var isHighlighted by rememberSaveable { mutableStateOf(false) } + SideEffect { + isHighlighted = entryData.isHighlighted + } + + val backgroundColor by animateColorAsState( + targetValue = when { + isHighlighted -> 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/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/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/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/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/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/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/layout/status_bar_expanded.xml b/packages/SystemUI/res/layout/status_bar_expanded.xml index 92ef3f8201cb..159323a5b557 100644 --- a/packages/SystemUI/res/layout/status_bar_expanded.xml +++ b/packages/SystemUI/res/layout/status_bar_expanded.xml @@ -42,12 +42,6 @@ android:clipToPadding="false" android:clipChildren="false"> - <ViewStub - android:id="@+id/qs_header_stub" - android:layout_height="wrap_content" - android:layout_width="match_parent" - /> - <include layout="@layout/keyguard_status_view" android:visibility="gone"/> @@ -69,6 +63,15 @@ systemui:layout_constraintBottom_toBottomOf="parent" /> + <!-- This view should be after qs_frame so touches are dispatched first to it. That gives + it a chance to capture clicks before the NonInterceptingScrollView disallows all + intercepts --> + <ViewStub + android:id="@+id/qs_header_stub" + android:layout_height="wrap_content" + android:layout_width="match_parent" + /> + <androidx.constraintlayout.widget.Guideline android:id="@+id/qs_edge_guideline" android:layout_width="wrap_content" 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 d5d99ccc9917..b325c56adefc 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2218,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/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/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 cc1f2fe5b027..39dc609c9bb7 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -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); } /** @@ -2357,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(); } diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt index 31fc3204a7f1..3308f5550bfc 100644 --- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt +++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt @@ -111,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) { diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationController.java index f56a15427452..ee048e1a02d3 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationController.java @@ -172,4 +172,8 @@ class DismissAnimationController implements ComponentCallbacks { 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 eb252323907d..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; /** @@ -64,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 @@ -102,6 +105,11 @@ class MenuAnimationController { } } + void setDismissCallback( + DismissAnimationController.DismissCallback dismissCallback) { + mDismissCallback = dismissCallback; + } + void moveToTopLeftPosition() { mIsMovedToEdge = false; final Rect draggableBounds = mMenuView.getMenuDraggableBounds(); @@ -132,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 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/MenuViewLayer.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java index 3e620a26f42c..33e155df80e3 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java @@ -100,6 +100,7 @@ class MenuViewLayer extends FrameLayout { windowManager); mMenuView = new MenuView(context, menuViewModel, menuViewAppearance); mMenuAnimationController = mMenuView.getMenuAnimationController(); + mMenuAnimationController.setDismissCallback(this::hideMenuAndShowMessage); mDismissView = new DismissView(context); mDismissAnimationController = new DismissAnimationController(mDismissView, mMenuView); 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 9c78df598857..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: @@ -693,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, @@ -717,6 +725,9 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, mFaceEnrolledForUser = new SparseBooleanArray(); mVibratorHelper = vibrator; + mBiometricPromptInteractor = biometricPromptInteractor; + mCredentialViewModelProvider = credentialViewModelProvider; + mOrientationListener = new BiometricDisplayListener( context, mDisplayManager, @@ -1079,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; @@ -1210,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 e47e636fa445..6db562107357 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -84,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; @@ -150,6 +151,7 @@ import dagger.Provides; SysUIConcurrencyModule.class, SysUIUnfoldModule.class, TelephonyRepositoryModule.class, + TemporaryDisplayModule.class, TunerModule.class, UserModule.class, UtilModule.class, 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 4818bccb1f64..fa499687fa21 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. @@ -124,6 +124,10 @@ object Flags { */ @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) @@ -196,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 @@ -230,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 = @@ -320,7 +318,7 @@ object Flags { // 1500 - chooser // TODO(b/254512507): Tracking Bug - val CHOOSER_UNBUNDLED = UnreleasedFlag(1500) + val CHOOSER_UNBUNDLED = UnreleasedFlag(1500, true) // 1600 - accessibility @JvmField val A11Y_FLOATING_MENU_FLING_SPRING_ANIMATIONS = UnreleasedFlag(1600) 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/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/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/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/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/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/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 66be6ecd9da4..680c3b8f546b 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java @@ -676,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(); @@ -688,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(); @@ -707,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()); } @@ -1695,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(), 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/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 88a806d93243..4dd46edd0912 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java @@ -89,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; @@ -165,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 @@ -1009,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/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/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/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/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/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/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/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/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/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index c54d7d1afe66..3032f17c2597 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -3891,24 +3891,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) { @@ -13860,6 +13865,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 @@ -18110,6 +18118,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 +18130,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 9ec1fb42ce5a..f7d24e9b8b4e 100644 --- a/services/core/java/com/android/server/am/BroadcastProcessQueue.java +++ b/services/core/java/com/android/server/am/BroadcastProcessQueue.java @@ -831,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()) { @@ -847,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/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java index ab19ae33a502..9e9eb71db4e5 100644 --- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java +++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java @@ -365,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) { 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/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 628c16afed5c..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,7 +385,8 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi BiometricsProtoEnums.CLIENT_UNKNOWN), mBiometricContext, mSensors.get(sensorId).getSensorProperties(), - mUdfpsOverlayController, mSidefpsController, maxTemplatesPerUser, enrollReason); + mUdfpsOverlayController, mSidefpsController, mUdfpsOverlay, + maxTemplatesPerUser, enrollReason); scheduleForSensor(sensorId, client, new ClientMonitorCompositeCallback( mBiometricStateCallback, new ClientMonitorCallback() { @Override @@ -418,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); }); @@ -439,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); }); } @@ -649,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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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) } |