Merge "Prevents NPE after stack got removed from display"
diff --git a/api/current.txt b/api/current.txt
index 9a3c681..de5a0b1 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -10238,6 +10238,7 @@
field public static final java.lang.String EXTRA_ASSIST_INPUT_HINT_KEYBOARD = "android.intent.extra.ASSIST_INPUT_HINT_KEYBOARD";
field public static final java.lang.String EXTRA_ASSIST_PACKAGE = "android.intent.extra.ASSIST_PACKAGE";
field public static final java.lang.String EXTRA_ASSIST_UID = "android.intent.extra.ASSIST_UID";
+ field public static final java.lang.String EXTRA_AUTO_LAUNCH_SINGLE_CHOICE = "android.intent.extra.AUTO_LAUNCH_SINGLE_CHOICE";
field public static final java.lang.String EXTRA_BCC = "android.intent.extra.BCC";
field public static final java.lang.String EXTRA_BUG_REPORT = "android.intent.extra.BUG_REPORT";
field public static final java.lang.String EXTRA_CC = "android.intent.extra.CC";
@@ -23396,6 +23397,7 @@
method public int getStreamType();
method public boolean getTimestamp(android.media.AudioTimestamp);
method public int getUnderrunCount();
+ method public static boolean isDirectPlaybackSupported(android.media.AudioFormat, android.media.AudioAttributes);
method public void pause() throws java.lang.IllegalStateException;
method public void play() throws java.lang.IllegalStateException;
method public void registerStreamEventCallback(java.util.concurrent.Executor, android.media.AudioTrack.StreamEventCallback);
@@ -24753,6 +24755,7 @@
field public static final java.lang.String MIMETYPE_AUDIO_AMR_NB = "audio/3gpp";
field public static final java.lang.String MIMETYPE_AUDIO_AMR_WB = "audio/amr-wb";
field public static final java.lang.String MIMETYPE_AUDIO_EAC3 = "audio/eac3";
+ field public static final java.lang.String MIMETYPE_AUDIO_EAC3_JOC = "audio/eac3-joc";
field public static final java.lang.String MIMETYPE_AUDIO_FLAC = "audio/flac";
field public static final java.lang.String MIMETYPE_AUDIO_G711_ALAW = "audio/g711-alaw";
field public static final java.lang.String MIMETYPE_AUDIO_G711_MLAW = "audio/g711-mlaw";
diff --git a/config/hiddenapi-greylist.txt b/config/hiddenapi-greylist.txt
index 56dc4c1..bacb991 100644
--- a/config/hiddenapi-greylist.txt
+++ b/config/hiddenapi-greylist.txt
@@ -1639,10 +1639,6 @@
Landroid/widget/QuickContactBadge$QueryHandler;-><init>(Landroid/widget/QuickContactBadge;Landroid/content/ContentResolver;)V
Landroid/widget/RelativeLayout$DependencyGraph$Node;-><init>()V
Landroid/widget/ScrollBarDrawable;-><init>()V
-Lcom/android/i18n/phonenumbers/Phonenumber$PhoneNumber$CountryCodeSource;->values()[Lcom/android/i18n/phonenumbers/Phonenumber$PhoneNumber$CountryCodeSource;
-Lcom/android/i18n/phonenumbers/PhoneNumberUtil$MatchType;->values()[Lcom/android/i18n/phonenumbers/PhoneNumberUtil$MatchType;
-Lcom/android/i18n/phonenumbers/PhoneNumberUtil$PhoneNumberFormat;->values()[Lcom/android/i18n/phonenumbers/PhoneNumberUtil$PhoneNumberFormat;
-Lcom/android/i18n/phonenumbers/PhoneNumberUtil$PhoneNumberType;->values()[Lcom/android/i18n/phonenumbers/PhoneNumberUtil$PhoneNumberType;
Lcom/android/ims/ImsCall;->deflect(Ljava/lang/String;)V
Lcom/android/ims/ImsCall;->isMultiparty()Z
Lcom/android/ims/ImsCall;->reject(I)V
diff --git a/core/java/android/annotation/UnsupportedAppUsage.java b/core/java/android/annotation/UnsupportedAppUsage.java
index 65e3f25..ac3daaf 100644
--- a/core/java/android/annotation/UnsupportedAppUsage.java
+++ b/core/java/android/annotation/UnsupportedAppUsage.java
@@ -26,16 +26,32 @@
import java.lang.annotation.Target;
/**
- * Indicates that a class member, that is not part of the SDK, is used by apps.
- * Since the member is not part of the SDK, such use is not supported.
+ * Indicates that this non-SDK interface is used by apps. A non-SDK interface is a
+ * class member (field or method) that is not part of the public SDK. Since the
+ * member is not part of the SDK, usage by apps is not supported.
*
- * <p>This annotation acts as a heads up that changing a given method or field
+ * <h2>If you are an Android App developer</h2>
+ *
+ * This annotation indicates that you may be able to access the member, but that
+ * this access is discouraged and not supported by Android. If there is a value
+ * for {@link #maxTargetSdk()} on the annotation, access will be restricted based
+ * on the {@code targetSdkVersion} value set in your manifest.
+ *
+ * <p>Fields and methods annotated with this are likely to be restricted, changed
+ * or removed in future Android releases. If you rely on these members for
+ * functionality that is not otherwise supported by Android, consider filing a
+ * <a href="http://g.co/dev/appcompat">feature request</a>.
+ *
+ * <h2>If you are an Android OS developer</h2>
+ *
+ * This annotation acts as a heads up that changing a given method or field
* may affect apps, potentially breaking them when the next Android version is
* released. In some cases, for members that are heavily used, this annotation
* may imply restrictions on changes to the member.
*
* <p>This annotation also results in access to the member being permitted by the
- * runtime, with a warning being generated in debug builds.
+ * runtime, with a warning being generated in debug builds. Which apps can access
+ * the member is determined by the value of {@link #maxTargetSdk()}.
*
* <p>For more details, see go/UnsupportedAppUsage.
*
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index b6f9d15..2f0618c 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -5267,8 +5267,6 @@
* Used as a boolean extra field in {@link #ACTION_CHOOSER} intents to specify
* whether to show the chooser or not when there is only one application available
* to choose from.
- *
- * @hide
*/
public static final String EXTRA_AUTO_LAUNCH_SINGLE_CHOICE =
"android.intent.extra.AUTO_LAUNCH_SINGLE_CHOICE";
diff --git a/core/java/android/content/pm/SharedLibraryNames.java b/core/java/android/content/pm/SharedLibraryNames.java
index 5afc8a9..a607a9f 100644
--- a/core/java/android/content/pm/SharedLibraryNames.java
+++ b/core/java/android/content/pm/SharedLibraryNames.java
@@ -22,15 +22,15 @@
*/
public class SharedLibraryNames {
- public static final String ANDROID_HIDL_BASE = "android.hidl.base-V1.0-java";
+ static final String ANDROID_HIDL_BASE = "android.hidl.base-V1.0-java";
- public static final String ANDROID_HIDL_MANAGER = "android.hidl.manager-V1.0-java";
+ static final String ANDROID_HIDL_MANAGER = "android.hidl.manager-V1.0-java";
- public static final String ANDROID_TEST_BASE = "android.test.base";
+ static final String ANDROID_TEST_BASE = "android.test.base";
- public static final String ANDROID_TEST_MOCK = "android.test.mock";
+ static final String ANDROID_TEST_MOCK = "android.test.mock";
- public static final String ANDROID_TEST_RUNNER = "android.test.runner";
+ static final String ANDROID_TEST_RUNNER = "android.test.runner";
public static final String ORG_APACHE_HTTP_LEGACY = "org.apache.http.legacy";
}
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index de6afcb..9380695 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -12852,6 +12852,13 @@
public static final String HIDDEN_API_POLICY = "hidden_api_policy";
/**
+ * Current version of signed configuration applied.
+ *
+ * @hide
+ */
+ public static final String SIGNED_CONFIG_VERSION = "signed_config_version";
+
+ /**
* Timeout for a single {@link android.media.soundtrigger.SoundTriggerDetectionService}
* operation (in ms).
*
diff --git a/core/java/com/android/server/SystemConfig.java b/core/java/com/android/server/SystemConfig.java
index b97a9fa..b00e6fd 100644
--- a/core/java/com/android/server/SystemConfig.java
+++ b/core/java/com/android/server/SystemConfig.java
@@ -25,7 +25,6 @@
import android.os.Build;
import android.os.Environment;
import android.os.Process;
-import android.os.SystemProperties;
import android.os.storage.StorageManager;
import android.permission.PermissionManager.SplitPermissionInfo;
import android.text.TextUtils;
@@ -78,10 +77,23 @@
final ArrayList<SplitPermissionInfo> mSplitPermissions = new ArrayList<>();
+ public static final class SharedLibraryEntry {
+ public final String name;
+ public final String filename;
+ public final String[] dependencies;
+
+ SharedLibraryEntry(String name, String filename, String[] dependencies) {
+ this.name = name;
+ this.filename = filename;
+ this.dependencies = dependencies;
+ }
+ }
+
// These are the built-in shared libraries that were read from the
- // system configuration files. Keys are the library names; strings are the
- // paths to the libraries.
- final ArrayMap<String, String> mSharedLibraries = new ArrayMap<>();
+ // system configuration files. Keys are the library names; values are
+ // the individual entries that contain information such as filename
+ // and dependencies.
+ final ArrayMap<String, SharedLibraryEntry> mSharedLibraries = new ArrayMap<>();
// These are the features this devices supports that were read from the
// system configuration files.
@@ -200,7 +212,7 @@
return mSplitPermissions;
}
- public ArrayMap<String, String> getSharedLibraries() {
+ public ArrayMap<String, SharedLibraryEntry> getSharedLibraries() {
return mSharedLibraries;
}
@@ -497,6 +509,7 @@
} else if ("library".equals(name) && allowLibs) {
String lname = parser.getAttributeValue(null, "name");
String lfile = parser.getAttributeValue(null, "file");
+ String ldependency = parser.getAttributeValue(null, "dependency");
if (lname == null) {
Slog.w(TAG, "<library> without name in " + permFile + " at "
+ parser.getPositionDescription());
@@ -505,11 +518,12 @@
+ parser.getPositionDescription());
} else {
//Log.i(TAG, "Got library " + lname + " in " + lfile);
- mSharedLibraries.put(lname, lfile);
+ SharedLibraryEntry entry = new SharedLibraryEntry(lname, lfile,
+ ldependency == null ? new String[0] : ldependency.split(":"));
+ mSharedLibraries.put(lname, entry);
}
XmlUtils.skipCurrentTag(parser);
continue;
-
} else if ("feature".equals(name) && allowFeatures) {
String fname = parser.getAttributeValue(null, "name");
int fversion = XmlUtils.readIntAttribute(parser, "version", 0);
diff --git a/core/jni/android_media_AudioTrack.cpp b/core/jni/android_media_AudioTrack.cpp
index 7ebb2b2..516093e 100644
--- a/core/jni/android_media_AudioTrack.cpp
+++ b/core/jni/android_media_AudioTrack.cpp
@@ -479,6 +479,24 @@
}
// ----------------------------------------------------------------------------
+static jboolean
+android_media_AudioTrack_is_direct_output_supported(JNIEnv *env, jobject thiz,
+ jint encoding, jint sampleRate,
+ jint channelMask, jint channelIndexMask,
+ jint contentType, jint usage, jint flags) {
+ audio_config_base_t config = {};
+ audio_attributes_t attributes = {};
+ config.format = static_cast<audio_format_t>(audioFormatToNative(encoding));
+ config.sample_rate = static_cast<uint32_t>(sampleRate);
+ config.channel_mask = nativeChannelMaskFromJavaChannelMasks(channelMask, channelIndexMask);
+ attributes.content_type = static_cast<audio_content_type_t>(contentType);
+ attributes.usage = static_cast<audio_usage_t>(usage);
+ attributes.flags = static_cast<audio_flags_mask_t>(flags);
+ // ignore source and tags attributes as they don't affect querying whether output is supported
+ return AudioTrack::isDirectOutputSupported(config, attributes);
+}
+
+// ----------------------------------------------------------------------------
static void
android_media_AudioTrack_start(JNIEnv *env, jobject thiz)
{
@@ -1297,6 +1315,9 @@
// ----------------------------------------------------------------------------
static const JNINativeMethod gMethods[] = {
// name, signature, funcPtr
+ {"native_is_direct_output_supported",
+ "(IIIIIII)Z",
+ (void *)android_media_AudioTrack_is_direct_output_supported},
{"native_start", "()V", (void *)android_media_AudioTrack_start},
{"native_stop", "()V", (void *)android_media_AudioTrack_stop},
{"native_pause", "()V", (void *)android_media_AudioTrack_pause},
diff --git a/core/proto/android/stats/devicepolicy/device_policy_enums.proto b/core/proto/android/stats/devicepolicy/device_policy_enums.proto
index 5726d9a..c7a6b68 100644
--- a/core/proto/android/stats/devicepolicy/device_policy_enums.proto
+++ b/core/proto/android/stats/devicepolicy/device_policy_enums.proto
@@ -51,7 +51,7 @@
SET_ALWAYS_ON_VPN_PACKAGE = 26;
SET_PERMITTED_INPUT_METHODS = 27;
SET_PERMITTED_ACCESSIBILITY_SERVICES = 28;
- SET_SCREEN_CAPTURE_DISABLE = 29;
+ SET_SCREEN_CAPTURE_DISABLED = 29;
SET_CAMERA_DISABLED = 30;
QUERY_SUMMARY_FOR_USER = 31;
QUERY_SUMMARY = 32;
@@ -64,7 +64,7 @@
SET_ORGANIZATION_COLOR = 39;
SET_PROFILE_NAME = 40;
SET_USER_ICON = 41;
- SET_DEVICE_OWNER_LOCKSCREEN_INFO = 42;
+ SET_DEVICE_OWNER_LOCK_SCREEN_INFO = 42;
SET_SHORT_SUPPORT_MESSAGE = 43;
SET_LONG_SUPPORT_MESSAGE = 44;
SET_CROSS_PROFILE_CONTACTS_SEARCH_DISABLED = 45;
@@ -139,4 +139,6 @@
SET_GLOBAL_SETTING = 111;
PM_IS_INSTALLER_DEVICE_OWNER_OR_AFFILIATED_PROFILE_OWNER = 112;
PM_UNINSTALL = 113;
+ WIFI_SERVICE_ADD_NETWORK_SUGGESTIONS = 114;
+ WIFI_SERVICE_ADD_OR_UPDATE_NETWORK = 115;
}
diff --git a/core/tests/coretests/src/android/provider/SettingsBackupTest.java b/core/tests/coretests/src/android/provider/SettingsBackupTest.java
index 49a7555..3a37fb6 100644
--- a/core/tests/coretests/src/android/provider/SettingsBackupTest.java
+++ b/core/tests/coretests/src/android/provider/SettingsBackupTest.java
@@ -415,6 +415,7 @@
Settings.Global.SHOW_NOTIFICATION_CHANNEL_WARNINGS,
Settings.Global.SHOW_RESTART_IN_CRASH_DIALOG,
Settings.Global.SHOW_TEMPERATURE_WARNING,
+ Settings.Global.SIGNED_CONFIG_VERSION,
Settings.Global.SMART_SELECTION_UPDATE_CONTENT_URL,
Settings.Global.SMART_SELECTION_UPDATE_METADATA_URL,
Settings.Global.SMS_ACCESS_RESTRICTION_ENABLED,
diff --git a/core/tests/coretests/src/android/security/keystore/recovery/KeyChainSnapshotTest.java b/core/tests/coretests/src/android/security/keystore/recovery/KeyChainSnapshotTest.java
index 8d6fbd5..61ab152 100644
--- a/core/tests/coretests/src/android/security/keystore/recovery/KeyChainSnapshotTest.java
+++ b/core/tests/coretests/src/android/security/keystore/recovery/KeyChainSnapshotTest.java
@@ -28,7 +28,8 @@
import org.junit.Test;
import org.junit.runner.RunWith;
-// TODO(b/73862682): Add tests for RecoveryCertPath
+import java.security.cert.CertPath;
+
@RunWith(AndroidJUnit4.class)
@SmallTest
public class KeyChainSnapshotTest {
@@ -43,35 +44,41 @@
private static final int USER_SECRET_TYPE = KeyChainProtectionParams.TYPE_LOCKSCREEN;
private static final String KEY_ALIAS = "steph";
private static final byte[] KEY_MATERIAL = new byte[] { 3, 5, 7, 9, 1 };
+ private static final CertPath CERT_PATH = TestData.getThmCertPath();
@Test
- public void build_setsCounterId() {
+ public void build_setsCounterId() throws Exception {
assertEquals(COUNTER_ID, createKeyChainSnapshot().getCounterId());
}
@Test
- public void build_setsSnapshotVersion() {
+ public void build_setsSnapshotVersion() throws Exception {
assertEquals(SNAPSHOT_VERSION, createKeyChainSnapshot().getSnapshotVersion());
}
@Test
- public void build_setsMaxAttempts() {
+ public void build_setsMaxAttempts() throws Exception {
assertEquals(MAX_ATTEMPTS, createKeyChainSnapshot().getMaxAttempts());
}
@Test
- public void build_setsServerParams() {
+ public void build_setsServerParams() throws Exception {
assertArrayEquals(SERVER_PARAMS, createKeyChainSnapshot().getServerParams());
}
@Test
- public void build_setsRecoveryKeyBlob() {
+ public void build_setsRecoveryKeyBlob() throws Exception {
assertArrayEquals(RECOVERY_KEY_BLOB,
createKeyChainSnapshot().getEncryptedRecoveryKeyBlob());
}
@Test
- public void build_setsKeyChainProtectionParams() {
+ public void build_setsCertPath() throws Exception {
+ assertEquals(CERT_PATH, createKeyChainSnapshot().getTrustedHardwareCertPath());
+ }
+
+ @Test
+ public void build_setsKeyChainProtectionParams() throws Exception {
KeyChainSnapshot snapshot = createKeyChainSnapshot();
assertEquals(1, snapshot.getKeyChainProtectionParams().size());
@@ -85,7 +92,7 @@
}
@Test
- public void build_setsWrappedApplicationKeys() {
+ public void build_setsWrappedApplicationKeys() throws Exception {
KeyChainSnapshot snapshot = createKeyChainSnapshot();
assertEquals(1, snapshot.getWrappedApplicationKeys().size());
@@ -95,42 +102,49 @@
}
@Test
- public void writeToParcel_writesCounterId() {
+ public void writeToParcel_writesCounterId() throws Exception {
KeyChainSnapshot snapshot = writeToThenReadFromParcel(createKeyChainSnapshot());
assertEquals(COUNTER_ID, snapshot.getCounterId());
}
@Test
- public void writeToParcel_writesSnapshotVersion() {
+ public void writeToParcel_writesSnapshotVersion() throws Exception {
KeyChainSnapshot snapshot = writeToThenReadFromParcel(createKeyChainSnapshot());
assertEquals(SNAPSHOT_VERSION, snapshot.getSnapshotVersion());
}
@Test
- public void writeToParcel_writesMaxAttempts() {
+ public void writeToParcel_writesMaxAttempts() throws Exception {
KeyChainSnapshot snapshot = writeToThenReadFromParcel(createKeyChainSnapshot());
assertEquals(MAX_ATTEMPTS, snapshot.getMaxAttempts());
}
@Test
- public void writeToParcel_writesServerParams() {
+ public void writeToParcel_writesServerParams() throws Exception {
KeyChainSnapshot snapshot = writeToThenReadFromParcel(createKeyChainSnapshot());
assertArrayEquals(SERVER_PARAMS, snapshot.getServerParams());
}
@Test
- public void writeToParcel_writesKeyRecoveryBlob() {
+ public void writeToParcel_writesKeyRecoveryBlob() throws Exception {
KeyChainSnapshot snapshot = writeToThenReadFromParcel(createKeyChainSnapshot());
assertArrayEquals(RECOVERY_KEY_BLOB, snapshot.getEncryptedRecoveryKeyBlob());
}
@Test
- public void writeToParcel_writesKeyChainProtectionParams() {
+ public void writeToParcel_writesCertPath() throws Exception {
+ KeyChainSnapshot snapshot = writeToThenReadFromParcel(createKeyChainSnapshot());
+
+ assertEquals(CERT_PATH, snapshot.getTrustedHardwareCertPath());
+ }
+
+ @Test
+ public void writeToParcel_writesKeyChainProtectionParams() throws Exception {
KeyChainSnapshot snapshot = writeToThenReadFromParcel(createKeyChainSnapshot());
assertEquals(1, snapshot.getKeyChainProtectionParams().size());
@@ -144,7 +158,7 @@
}
@Test
- public void writeToParcel_writesWrappedApplicationKeys() {
+ public void writeToParcel_writesWrappedApplicationKeys() throws Exception {
KeyChainSnapshot snapshot = writeToThenReadFromParcel(createKeyChainSnapshot());
assertEquals(1, snapshot.getWrappedApplicationKeys().size());
@@ -153,7 +167,7 @@
assertArrayEquals(KEY_MATERIAL, wrappedApplicationKey.getEncryptedKeyMaterial());
}
- private static KeyChainSnapshot createKeyChainSnapshot() {
+ private static KeyChainSnapshot createKeyChainSnapshot() throws Exception {
return new KeyChainSnapshot.Builder()
.setCounterId(COUNTER_ID)
.setSnapshotVersion(SNAPSHOT_VERSION)
@@ -162,6 +176,7 @@
.setEncryptedRecoveryKeyBlob(RECOVERY_KEY_BLOB)
.setKeyChainProtectionParams(Lists.newArrayList(createKeyChainProtectionParams()))
.setWrappedApplicationKeys(Lists.newArrayList(createWrappedApplicationKey()))
+ .setTrustedHardwareCertPath(CERT_PATH)
.build();
}
diff --git a/core/tests/coretests/src/android/security/keystore/recovery/RecoveryCertPathTest.java b/core/tests/coretests/src/android/security/keystore/recovery/RecoveryCertPathTest.java
new file mode 100644
index 0000000..dd8cd8d
--- /dev/null
+++ b/core/tests/coretests/src/android/security/keystore/recovery/RecoveryCertPathTest.java
@@ -0,0 +1,74 @@
+/*
+ * 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 android.security.keystore.recovery;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import android.os.Parcel;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.security.cert.CertificateException;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class RecoveryCertPathTest {
+
+ @Test
+ public void createRecoveryCertPath_getCertPath_succeeds() throws Exception {
+ RecoveryCertPath recoveryCertPath = RecoveryCertPath.createRecoveryCertPath(
+ TestData.getThmCertPath());
+ assertEquals(TestData.getThmCertPath(), recoveryCertPath.getCertPath());
+ }
+
+ @Test
+ public void getCertPath_throwsIfCannnotDecode() {
+ Parcel parcel = Parcel.obtain();
+ parcel.writeByteArray(new byte[]{0, 1, 2, 3});
+ parcel.setDataPosition(0);
+ RecoveryCertPath recoveryCertPath = RecoveryCertPath.CREATOR.createFromParcel(parcel);
+ parcel.recycle();
+
+ try {
+ recoveryCertPath.getCertPath();
+ fail("Did not throw when attempting to decode invalid cert path");
+ } catch (CertificateException e) {
+ // Expected
+ }
+ }
+
+ @Test
+ public void writeToParcel_writesCertPath() throws Exception {
+ RecoveryCertPath recoveryCertPath =
+ writeToThenReadFromParcel(
+ RecoveryCertPath.createRecoveryCertPath(TestData.getThmCertPath()));
+ assertEquals(TestData.getThmCertPath(), recoveryCertPath.getCertPath());
+ }
+
+ private RecoveryCertPath writeToThenReadFromParcel(RecoveryCertPath recoveryCertPath) {
+ Parcel parcel = Parcel.obtain();
+ recoveryCertPath.writeToParcel(parcel, /*flags=*/ 0);
+ parcel.setDataPosition(0);
+ RecoveryCertPath fromParcel = RecoveryCertPath.CREATOR.createFromParcel(parcel);
+ parcel.recycle();
+ return fromParcel;
+ }
+}
diff --git a/core/tests/coretests/src/android/security/keystore/recovery/TestData.java b/core/tests/coretests/src/android/security/keystore/recovery/TestData.java
new file mode 100644
index 0000000..829a92a
--- /dev/null
+++ b/core/tests/coretests/src/android/security/keystore/recovery/TestData.java
@@ -0,0 +1,88 @@
+/*
+ * 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 android.security.keystore.recovery;
+
+import java.io.ByteArrayInputStream;
+import java.security.cert.CertPath;
+import java.security.cert.CertificateFactory;
+import java.util.Base64;
+
+/** This class provides data for testing purposes. */
+class TestData {
+
+ private static final String THM_CERT_PATH_BASE64 = ""
+ + "MIIIXTCCBRowggMCoAMCAQICEB35ZwzVpI9ssXg9SAehnU0wDQYJKoZIhvcNAQEL"
+ + "BQAwMTEvMC0GA1UEAxMmR29vZ2xlIENsb3VkIEtleSBWYXVsdCBTZXJ2aWNlIFJv"
+ + "b3QgQ0EwHhcNMTgwNTA3MTg1ODEwWhcNMjgwNTA4MTg1ODEwWjA5MTcwNQYDVQQD"
+ + "Ey5Hb29nbGUgQ2xvdWQgS2V5IFZhdWx0IFNlcnZpY2UgSW50ZXJtZWRpYXRlIENB"
+ + "MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA73TrvH3j6zEimpcc32tx"
+ + "2iupWwfyzdE5l4Ejc5EBYzx0aZH6b/KDuutwustk0IoyjlGySMBz/21YgWejIm+n"
+ + "duAlpk7WY5kYHp0XWtzdmxZknmWTqugPeNZeiKEjoDmpyIbY6N+f13hQ2RVh+WDT"
+ + "EowQ/i04WBL75chshlIG+3A42g5Qr7DZEKdT9oJQqkntzj0cGyJ5X8BwjeTiJrvY"
+ + "k2Kn/0555/Kpp65G3Rf29VPPU3i67kthAT3SavLBpH03S4WZ+QlfrAiGQziydtz9"
+ + "t7mSk1xefjax5ZWAuJAfCbKfI3VWAcaUr4P57BzmDcSi0jgs1aM3t2BrPfAMRxWv"
+ + "35yDZnrC+HipzkjyDGBfHmFgoglyhc9e/Kj3mSusO0Rq1wguVXKs2hKXRoaGJuHt"
+ + "e3YIwTC1pLznqvolhD1nPoXf8rMzgHRzlc9H8iXsgB1p7975nh5WCPrMDX2eAmYd"
+ + "a0xTMccTeBzIM2ohxQsxlh5rsjXVNU3ihbWkHquzIiwFcAtldP3dMksj0dn/DnYD"
+ + "yokjEgU/z2I216E93x9hmKkEk6Pp7o8t/z6lwMT9FJIuzp7NREnWCSi+e5s2E7FD"
+ + "j6S7xY2zEIUHrmwuuJc0jzJnwdZ+0myinaTmBDvBXR5cU1cmEAZoheCAoRv9Z/6o"
+ + "ASczLF0C4uuVfA5GXcAm14cCAwEAAaMmMCQwDgYDVR0PAQH/BAQDAgGGMBIGA1Ud"
+ + "EwEB/wQIMAYBAf8CAQEwDQYJKoZIhvcNAQELBQADggIBAEPht79yQm8woQbPB1Bs"
+ + "eotkzJtTWTO9fnIWwNiRfQ3vJFXf69ghE77wUS13Ez3FlgNPj0Qxmg5ouE0d2yYV"
+ + "4AUrXnEGZELcyN2XHRXyNK0zXgnr3x6eZyY7QfgGKJgkyja5TS6ZPWGyaLKhClS0"
+ + "AYZSzWJtz0+AkGCdTbmyy7ShdXJ+GfnmssbndZA62VhcjeQmHsDq7V3PKAsp4/B9"
+ + "PzcnTrgkUFNnP1F1pr7JpUUX3xyRFy6gjIrUx1fcOFRxFYPWGLLMZ6P41rafm+M/"
+ + "CbBNr5CY7NrZjr34jLqWycfYes49o9OK44X/wPrxj0Sjg+VrW21+AJ9vrM7DS5hE"
+ + "QX1lDbDtQGkk3N1vgCTo6xt9LXsEu4xUT5bk7YAfpJqM0ltDFPwYAGCbjSkVT/M5"
+ + "JVZkKiUW668Us67x8yZc/5bxbvTA+5xrYhak/VYIBY6qub4J+bKwadw6uBgxnq4P"
+ + "hwgwjfaoJy9YAXCswjCtaE9GwkVmRnJE9vFjJ33IGf37hFTYEHBFy4FomVmQwRFZ"
+ + "TIe7tkKDq9i18F7lzBPJPO6wEG8bxi4csatrjcVHR9erpY5u6ebtkKG8qsan9qzh"
+ + "iWAgSytiT++HejZeoQ+RRgQWjupjdDo5/0oSdQqvaN8Ah6C2J+ecCZ12Lu0FwF+t"
+ + "t9Ie3pF6W8TzxzuMdFWq+afvMIIDOzCCASOgAwIBAgIRAOTj/iNQb6/Qit7zAW9n"
+ + "cL0wDQYJKoZIhvcNAQELBQAwOTE3MDUGA1UEAxMuR29vZ2xlIENsb3VkIEtleSBW"
+ + "YXVsdCBTZXJ2aWNlIEludGVybWVkaWF0ZSBDQTAeFw0xODA1MDcyMjE4MTFaFw0y"
+ + "MzA1MDgyMjE4MTFaMDIxMDAuBgNVBAMTJ0dvb2dsZSBDbG91ZCBLZXkgVmF1bHQg"
+ + "U2VydmljZSBFbmRwb2ludDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABI4MEUp5"
+ + "IHwATNfpBuJYIUX6JMsHZt798YO0JlWYy6nVVa1lxf9c+xxONJh+T5aio370RlIE"
+ + "uiq5R7vCHt0VGsCjEDAOMAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIB"
+ + "AGf6QU58lU+gGzy8hnp0suR/ixC++CExtf39pDHkdfU/e3ui4ROR+pjQ5F7okDFW"
+ + "eKSCNcJZ7tyXMJ9g7/I0qVY8Bj/gRnlVokdl/wD5PiL9GIzqWfnHNe3T+xrAAAgO"
+ + "D0bEmjgwNYmekfUIYQczd04d7ZMGnmAkpVH/0O2mf9q5x9fMlbKuAygUqQ/gmnlg"
+ + "xKfl9DSRWi4oMBOqlKlCRP1XAh3anu92+M/EhsFbyc07CWZY0SByX5M/cHVMLhUX"
+ + "jZHvcYLyOmJWJmXznidgyNeIR6t9yDB55iCt7WSn3qMY+9vA9ELzt8jYpBNaKc0G"
+ + "bWQkRzYWegkf4kMis98eQ3SnAKbRz6669nmuAdxKs9/LK6BlFOFw1xvsTRQ96dBa"
+ + "oiX2XGhou+Im0Td/AMs0Aigz2N+Ujq/yW//35GZQfdGGIYtFbkcltStygjIJyAM1"
+ + "pBhyBBkJhOhRpO4fXh98aq8H5J7R9i5A9WpnDstAxPxcNCDWn0O/WxhPvVZkFTpi"
+ + "NXh9dnlJ/kZe+j+z5ZMaxW435drLPx2AQKjXA9GgGrFPltTUyGycmEGtuxLvSnm/"
+ + "zPlmk5FUk7x2wEr0+bZ3cx0JHHgAtgXpe0jkDi8Bw8O3X7mUOjxVhYU6auiYJezW"
+ + "9LGmweaKwYvS04UCWOReolUVexob9LI/VX1JrrwD3s7k";
+
+ static CertPath getThmCertPath() {
+ try {
+ return decodeCertPath(THM_CERT_PATH_BASE64);
+ } catch (Exception e) {
+ // Should never happen
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static CertPath decodeCertPath(String base64CertPath) throws Exception {
+ byte[] certPathBytes = Base64.getDecoder().decode(base64CertPath);
+ CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
+ return certFactory.generateCertPath(new ByteArrayInputStream(certPathBytes), "PkiPath");
+ }
+}
diff --git a/data/etc/platform.xml b/data/etc/platform.xml
index 141948f..4a2db0a 100644
--- a/data/etc/platform.xml
+++ b/data/etc/platform.xml
@@ -225,15 +225,18 @@
<library name="android.test.base"
file="/system/framework/android.test.base.impl.jar" />
<library name="android.test.mock"
- file="/system/framework/android.test.mock.impl.jar" />
+ file="/system/framework/android.test.mock.impl.jar"
+ dependency="android.test.base" />
<library name="android.test.runner"
- file="/system/framework/android.test.runner.impl.jar" />
+ file="/system/framework/android.test.runner.impl.jar"
+ dependency="android.test.base:android.test.mock" />
<!-- In BOOT_JARS historically, and now added to legacy applications. -->
<library name="android.hidl.base-V1.0-java"
file="/system/framework/android.hidl.base-V1.0-java.jar" />
<library name="android.hidl.manager-V1.0-java"
- file="/system/framework/android.hidl.manager-V1.0-java.jar" />
+ file="/system/framework/android.hidl.manager-V1.0-java.jar"
+ dependency="android.hidl.base-V1.0-java" />
<!-- These are the standard packages that are white-listed to always have internet
access while in power save mode, even if they aren't in the foreground. -->
diff --git a/libs/input/Android.bp b/libs/input/Android.bp
index f1d9397..a2e8672 100644
--- a/libs/input/Android.bp
+++ b/libs/input/Android.bp
@@ -14,7 +14,7 @@
cc_library_shared {
name: "libinputservice",
-
+ cpp_std: "c++17",
srcs: [
"PointerController.cpp",
"SpriteController.cpp",
diff --git a/libs/input/PointerController.cpp b/libs/input/PointerController.cpp
index 0a90f85..80d8e72 100644
--- a/libs/input/PointerController.cpp
+++ b/libs/input/PointerController.cpp
@@ -89,10 +89,6 @@
mLocked.animationPending = false;
- mLocked.displayWidth = -1;
- mLocked.displayHeight = -1;
- mLocked.displayOrientation = DISPLAY_ORIENTATION_0;
-
mLocked.presentation = PRESENTATION_POINTER;
mLocked.presentationChanged = false;
@@ -110,15 +106,6 @@
mLocked.lastFrameUpdatedTime = 0;
mLocked.buttonState = 0;
-
- mPolicy->loadPointerIcon(&mLocked.pointerIcon);
-
- loadResources();
-
- if (mLocked.pointerIcon.isValid()) {
- mLocked.pointerIconChanged = true;
- updatePointerLocked();
- }
}
PointerController::~PointerController() {
@@ -144,23 +131,15 @@
bool PointerController::getBoundsLocked(float* outMinX, float* outMinY,
float* outMaxX, float* outMaxY) const {
- if (mLocked.displayWidth <= 0 || mLocked.displayHeight <= 0) {
+
+ if (!mLocked.viewport.isValid()) {
return false;
}
- *outMinX = 0;
- *outMinY = 0;
- switch (mLocked.displayOrientation) {
- case DISPLAY_ORIENTATION_90:
- case DISPLAY_ORIENTATION_270:
- *outMaxX = mLocked.displayHeight - 1;
- *outMaxY = mLocked.displayWidth - 1;
- break;
- default:
- *outMaxX = mLocked.displayWidth - 1;
- *outMaxY = mLocked.displayHeight - 1;
- break;
- }
+ *outMinX = mLocked.viewport.logicalLeft;
+ *outMinY = mLocked.viewport.logicalTop;
+ *outMaxX = mLocked.viewport.logicalRight - 1;
+ *outMaxY = mLocked.viewport.logicalBottom - 1;
return true;
}
@@ -231,6 +210,12 @@
*outY = mLocked.pointerY;
}
+int32_t PointerController::getDisplayId() const {
+ AutoMutex _l(mLock);
+
+ return mLocked.viewport.displayId;
+}
+
void PointerController::fade(Transition transition) {
AutoMutex _l(mLock);
@@ -355,48 +340,57 @@
void PointerController::reloadPointerResources() {
AutoMutex _l(mLock);
- loadResources();
-
- if (mLocked.presentation == PRESENTATION_POINTER) {
- mLocked.additionalMouseResources.clear();
- mLocked.animationResources.clear();
- mPolicy->loadPointerIcon(&mLocked.pointerIcon);
- mPolicy->loadAdditionalMouseResources(&mLocked.additionalMouseResources,
- &mLocked.animationResources);
- }
-
- mLocked.presentationChanged = true;
+ loadResourcesLocked();
updatePointerLocked();
}
-void PointerController::setDisplayViewport(int32_t width, int32_t height, int32_t orientation) {
- AutoMutex _l(mLock);
+/**
+ * The viewport values for deviceHeight and deviceWidth have already been adjusted for rotation,
+ * so here we are getting the dimensions in the original, unrotated orientation (orientation 0).
+ */
+static void getNonRotatedSize(const DisplayViewport& viewport, int32_t& width, int32_t& height) {
+ if (viewport.orientation == DISPLAY_ORIENTATION_90
+ || viewport.orientation == DISPLAY_ORIENTATION_270) {
+ width = viewport.deviceHeight;
+ height = viewport.deviceWidth;
+ } else {
+ width = viewport.deviceWidth;
+ height = viewport.deviceHeight;
+ }
+}
- // Adjust to use the display's unrotated coordinate frame.
- if (orientation == DISPLAY_ORIENTATION_90
- || orientation == DISPLAY_ORIENTATION_270) {
- int32_t temp = height;
- height = width;
- width = temp;
+void PointerController::setDisplayViewport(const DisplayViewport& viewport) {
+ AutoMutex _l(mLock);
+ if (viewport == mLocked.viewport) {
+ return;
}
- if (mLocked.displayWidth != width || mLocked.displayHeight != height) {
- mLocked.displayWidth = width;
- mLocked.displayHeight = height;
+ const DisplayViewport oldViewport = mLocked.viewport;
+ mLocked.viewport = viewport;
+
+ int32_t oldDisplayWidth, oldDisplayHeight;
+ getNonRotatedSize(oldViewport, oldDisplayWidth, oldDisplayHeight);
+ int32_t newDisplayWidth, newDisplayHeight;
+ getNonRotatedSize(viewport, newDisplayWidth, newDisplayHeight);
+
+ // Reset cursor position to center if size or display changed.
+ if (oldViewport.displayId != viewport.displayId
+ || oldDisplayWidth != newDisplayWidth
+ || oldDisplayHeight != newDisplayHeight) {
float minX, minY, maxX, maxY;
if (getBoundsLocked(&minX, &minY, &maxX, &maxY)) {
mLocked.pointerX = (minX + maxX) * 0.5f;
mLocked.pointerY = (minY + maxY) * 0.5f;
+ // Reload icon resources for density may be changed.
+ loadResourcesLocked();
} else {
mLocked.pointerX = 0;
mLocked.pointerY = 0;
}
fadeOutAndReleaseAllSpotsLocked();
- }
-
- if (mLocked.displayOrientation != orientation) {
+ } else if (oldViewport.orientation != viewport.orientation) {
// Apply offsets to convert from the pixel top-left corner position to the pixel center.
// This creates an invariant frame of reference that we can easily rotate when
// taking into account that the pointer may be located at fractional pixel offsets.
@@ -405,37 +399,37 @@
float temp;
// Undo the previous rotation.
- switch (mLocked.displayOrientation) {
+ switch (oldViewport.orientation) {
case DISPLAY_ORIENTATION_90:
temp = x;
- x = mLocked.displayWidth - y;
+ x = oldViewport.deviceHeight - y;
y = temp;
break;
case DISPLAY_ORIENTATION_180:
- x = mLocked.displayWidth - x;
- y = mLocked.displayHeight - y;
+ x = oldViewport.deviceWidth - x;
+ y = oldViewport.deviceHeight - y;
break;
case DISPLAY_ORIENTATION_270:
temp = x;
x = y;
- y = mLocked.displayHeight - temp;
+ y = oldViewport.deviceWidth - temp;
break;
}
// Perform the new rotation.
- switch (orientation) {
+ switch (viewport.orientation) {
case DISPLAY_ORIENTATION_90:
temp = x;
x = y;
- y = mLocked.displayWidth - temp;
+ y = viewport.deviceHeight - temp;
break;
case DISPLAY_ORIENTATION_180:
- x = mLocked.displayWidth - x;
- y = mLocked.displayHeight - y;
+ x = viewport.deviceWidth - x;
+ y = viewport.deviceHeight - y;
break;
case DISPLAY_ORIENTATION_270:
temp = x;
- x = mLocked.displayHeight - y;
+ x = viewport.deviceWidth - y;
y = temp;
break;
}
@@ -444,7 +438,6 @@
// and save the results.
mLocked.pointerX = x - 0.5f;
mLocked.pointerY = y - 0.5f;
- mLocked.displayOrientation = orientation;
}
updatePointerLocked();
@@ -614,11 +607,16 @@
mLooper->removeMessages(mHandler, MSG_INACTIVITY_TIMEOUT);
}
-void PointerController::updatePointerLocked() {
+void PointerController::updatePointerLocked() REQUIRES(mLock) {
+ if (!mLocked.viewport.isValid()) {
+ return;
+ }
+
mSpriteController->openTransaction();
mLocked.pointerSprite->setLayer(Sprite::BASE_LAYER_POINTER);
mLocked.pointerSprite->setPosition(mLocked.pointerX, mLocked.pointerY);
+ mLocked.pointerSprite->setDisplayId(mLocked.viewport.displayId);
if (mLocked.pointerAlpha > 0) {
mLocked.pointerSprite->setAlpha(mLocked.pointerAlpha);
@@ -729,8 +727,18 @@
}
}
-void PointerController::loadResources() {
+void PointerController::loadResourcesLocked() REQUIRES(mLock) {
mPolicy->loadPointerResources(&mResources);
+
+ if (mLocked.presentation == PRESENTATION_POINTER) {
+ mLocked.additionalMouseResources.clear();
+ mLocked.animationResources.clear();
+ mPolicy->loadPointerIcon(&mLocked.pointerIcon);
+ mPolicy->loadAdditionalMouseResources(&mLocked.additionalMouseResources,
+ &mLocked.animationResources);
+ }
+
+ mLocked.pointerIconChanged = true;
}
diff --git a/libs/input/PointerController.h b/libs/input/PointerController.h
index 7f4e5a5..a32cc42 100644
--- a/libs/input/PointerController.h
+++ b/libs/input/PointerController.h
@@ -23,6 +23,7 @@
#include <vector>
#include <ui/DisplayInfo.h>
+#include <input/DisplayViewport.h>
#include <input/Input.h>
#include <PointerControllerInterface.h>
#include <utils/BitSet.h>
@@ -96,6 +97,7 @@
virtual int32_t getButtonState() const;
virtual void setPosition(float x, float y);
virtual void getPosition(float* outX, float* outY) const;
+ virtual int32_t getDisplayId() const;
virtual void fade(Transition transition);
virtual void unfade(Transition transition);
@@ -106,7 +108,7 @@
void updatePointerIcon(int32_t iconId);
void setCustomPointerIcon(const SpriteIcon& icon);
- void setDisplayViewport(int32_t width, int32_t height, int32_t orientation);
+ void setDisplayViewport(const DisplayViewport& viewport);
void setInactivityTimeout(InactivityTimeout inactivityTimeout);
void reloadPointerResources();
@@ -156,9 +158,7 @@
size_t animationFrameIndex;
nsecs_t lastFrameUpdatedTime;
- int32_t displayWidth;
- int32_t displayHeight;
- int32_t displayOrientation;
+ DisplayViewport viewport;
InactivityTimeout inactivityTimeout;
@@ -182,7 +182,7 @@
Vector<Spot*> spots;
Vector<sp<Sprite> > recycledSprites;
- } mLocked;
+ } mLocked GUARDED_BY(mLock);
bool getBoundsLocked(float* outMinX, float* outMinY, float* outMaxX, float* outMaxY) const;
void setPositionLocked(float x, float y);
@@ -207,7 +207,7 @@
void fadeOutAndReleaseSpotLocked(Spot* spot);
void fadeOutAndReleaseAllSpotsLocked();
- void loadResources();
+ void loadResourcesLocked();
};
} // namespace android
diff --git a/libs/input/SpriteController.cpp b/libs/input/SpriteController.cpp
index eb2bc98..c1868d3 100644
--- a/libs/input/SpriteController.cpp
+++ b/libs/input/SpriteController.cpp
@@ -144,13 +144,16 @@
}
}
- // Resize sprites if needed.
+ // Resize and/or reparent sprites if needed.
SurfaceComposerClient::Transaction t;
bool needApplyTransaction = false;
for (size_t i = 0; i < numSprites; i++) {
SpriteUpdate& update = updates.editItemAt(i);
+ if (update.state.surfaceControl == nullptr) {
+ continue;
+ }
- if (update.state.surfaceControl != NULL && update.state.wantSurfaceVisible()) {
+ if (update.state.wantSurfaceVisible()) {
int32_t desiredWidth = update.state.icon.bitmap.width();
int32_t desiredHeight = update.state.icon.bitmap.height();
if (update.state.surfaceWidth < desiredWidth
@@ -170,6 +173,12 @@
}
}
}
+
+ // If surface is a new one, we have to set right layer stack.
+ if (update.surfaceChanged || update.state.dirty & DIRTY_DISPLAY_ID) {
+ t.setLayerStack(update.state.surfaceControl, update.state.displayId);
+ needApplyTransaction = true;
+ }
}
if (needApplyTransaction) {
t.apply();
@@ -236,7 +245,7 @@
if (update.state.surfaceControl != NULL && (becomingVisible || becomingHidden
|| (wantSurfaceVisibleAndDrawn && (update.state.dirty & (DIRTY_ALPHA
| DIRTY_POSITION | DIRTY_TRANSFORMATION_MATRIX | DIRTY_LAYER
- | DIRTY_VISIBILITY | DIRTY_HOTSPOT))))) {
+ | DIRTY_VISIBILITY | DIRTY_HOTSPOT | DIRTY_DISPLAY_ID))))) {
needApplyTransaction = true;
if (wantSurfaceVisibleAndDrawn
@@ -445,6 +454,15 @@
}
}
+void SpriteController::SpriteImpl::setDisplayId(int32_t displayId) {
+ AutoMutex _l(mController->mLock);
+
+ if (mLocked.state.displayId != displayId) {
+ mLocked.state.displayId = displayId;
+ invalidateLocked(DIRTY_DISPLAY_ID);
+ }
+}
+
void SpriteController::SpriteImpl::invalidateLocked(uint32_t dirty) {
bool wasDirty = mLocked.state.dirty;
mLocked.state.dirty |= dirty;
diff --git a/libs/input/SpriteController.h b/libs/input/SpriteController.h
index 31e43e9..5b216f5 100644
--- a/libs/input/SpriteController.h
+++ b/libs/input/SpriteController.h
@@ -125,6 +125,9 @@
/* Sets the sprite transformation matrix. */
virtual void setTransformationMatrix(const SpriteTransformationMatrix& matrix) = 0;
+
+ /* Sets the id of the display where the sprite should be shown. */
+ virtual void setDisplayId(int32_t displayId) = 0;
};
/*
@@ -170,6 +173,7 @@
DIRTY_LAYER = 1 << 4,
DIRTY_VISIBILITY = 1 << 5,
DIRTY_HOTSPOT = 1 << 6,
+ DIRTY_DISPLAY_ID = 1 << 7,
};
/* Describes the state of a sprite.
@@ -180,7 +184,7 @@
struct SpriteState {
inline SpriteState() :
dirty(0), visible(false),
- positionX(0), positionY(0), layer(0), alpha(1.0f),
+ positionX(0), positionY(0), layer(0), alpha(1.0f), displayId(ADISPLAY_ID_DEFAULT),
surfaceWidth(0), surfaceHeight(0), surfaceDrawn(false), surfaceVisible(false) {
}
@@ -193,6 +197,7 @@
int32_t layer;
float alpha;
SpriteTransformationMatrix transformationMatrix;
+ int32_t displayId;
sp<SurfaceControl> surfaceControl;
int32_t surfaceWidth;
@@ -225,6 +230,7 @@
virtual void setLayer(int32_t layer);
virtual void setAlpha(float alpha);
virtual void setTransformationMatrix(const SpriteTransformationMatrix& matrix);
+ virtual void setDisplayId(int32_t displayId);
inline const SpriteState& getStateLocked() const {
return mLocked.state;
diff --git a/media/java/android/media/AudioTrack.java b/media/java/android/media/AudioTrack.java
index e4d4795..5e77fdf 100644
--- a/media/java/android/media/AudioTrack.java
+++ b/media/java/android/media/AudioTrack.java
@@ -995,6 +995,35 @@
}
}
+ /**
+ * Returns whether direct playback of an audio format with the provided attributes is
+ * currently supported on the system.
+ * <p>Direct playback means that the audio stream is not resampled or downmixed
+ * by the framework. Checking for direct support can help the app select the representation
+ * of audio content that most closely matches the capabilities of the device and peripherials
+ * (e.g. A/V receiver) connected to it. Note that the provided stream can still be re-encoded
+ * or mixed with other streams, if needed.
+ * <p>Also note that this query only provides information about the support of an audio format.
+ * It does not indicate whether the resources necessary for the playback are available
+ * at that instant.
+ * @param format a non-null {@link AudioFormat} instance describing the format of
+ * the audio data.
+ * @param attributes a non-null {@link AudioAttributes} instance.
+ * @return true if the given audio format can be played directly.
+ */
+ public static boolean isDirectPlaybackSupported(@NonNull AudioFormat format,
+ @NonNull AudioAttributes attributes) {
+ if (format == null) {
+ throw new IllegalArgumentException("Illegal null AudioFormat argument");
+ }
+ if (attributes == null) {
+ throw new IllegalArgumentException("Illegal null AudioAttributes argument");
+ }
+ return native_is_direct_output_supported(format.getEncoding(), format.getSampleRate(),
+ format.getChannelMask(), format.getChannelIndexMask(),
+ attributes.getContentType(), attributes.getUsage(), attributes.getFlags());
+ }
+
// mask of all the positional channels supported, however the allowed combinations
// are further restricted by the matching left/right rule and
// AudioSystem.OUT_CHANNEL_COUNT_MAX
@@ -3328,6 +3357,9 @@
// Native methods called from the Java side
//--------------------
+ private static native boolean native_is_direct_output_supported(int encoding, int sampleRate,
+ int channelMask, int channelIndexMask, int contentType, int usage, int flags);
+
// post-condition: mStreamType is overwritten with a value
// that reflects the audio attributes (e.g. an AudioAttributes object with a usage of
// AudioAttributes.USAGE_MEDIA will map to AudioManager.STREAM_MUSIC
diff --git a/media/java/android/media/MediaCodecInfo.java b/media/java/android/media/MediaCodecInfo.java
index 95e3df2..9025829 100644
--- a/media/java/android/media/MediaCodecInfo.java
+++ b/media/java/android/media/MediaCodecInfo.java
@@ -1135,6 +1135,10 @@
maxChannels = 6;
} else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_EAC3)) {
maxChannels = 16;
+ } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_EAC3_JOC)) {
+ sampleRates = new int[] { 48000 };
+ bitRates = Range.create(32000, 6144000);
+ maxChannels = 16;
} else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_AC4)) {
sampleRates = new int[] { 44100, 48000, 96000, 192000 };
bitRates = Range.create(16000, 2688000);
diff --git a/media/java/android/media/MediaFormat.java b/media/java/android/media/MediaFormat.java
index b62108f..594a224 100644
--- a/media/java/android/media/MediaFormat.java
+++ b/media/java/android/media/MediaFormat.java
@@ -147,6 +147,7 @@
public static final String MIMETYPE_AUDIO_MSGSM = "audio/gsm";
public static final String MIMETYPE_AUDIO_AC3 = "audio/ac3";
public static final String MIMETYPE_AUDIO_EAC3 = "audio/eac3";
+ public static final String MIMETYPE_AUDIO_EAC3_JOC = "audio/eac3-joc";
public static final String MIMETYPE_AUDIO_AC4 = "audio/ac4";
public static final String MIMETYPE_AUDIO_SCRAMBLED = "audio/scrambled";
diff --git a/packages/ExtServices/src/android/ext/services/notification/Assistant.java b/packages/ExtServices/src/android/ext/services/notification/Assistant.java
index 0cad5af..133d8ba 100644
--- a/packages/ExtServices/src/android/ext/services/notification/Assistant.java
+++ b/packages/ExtServices/src/android/ext/services/notification/Assistant.java
@@ -27,20 +27,15 @@
import android.app.INotificationManager;
import android.app.Notification;
import android.app.NotificationChannel;
-import android.content.ContentResolver;
import android.content.Context;
import android.content.pm.IPackageManager;
-import android.database.ContentObserver;
import android.ext.services.notification.AgingHelper.Callback;
-import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Environment;
-import android.os.Handler;
import android.os.SystemProperties;
import android.os.UserHandle;
import android.os.storage.StorageManager;
-import android.provider.Settings;
import android.service.notification.Adjustment;
import android.service.notification.NotificationAssistantService;
import android.service.notification.NotificationStats;
@@ -92,8 +87,6 @@
PREJUDICAL_DISMISSALS.add(REASON_LISTENER_CANCEL);
}
- private float mDismissToViewRatioLimit;
- private int mStreakLimit;
private SmartActionsHelper mSmartActionsHelper;
private NotificationCategorizer mNotificationCategorizer;
private AgingHelper mAgingHelper;
@@ -107,7 +100,11 @@
private Ranking mFakeRanking = null;
private AtomicFile mFile = null;
private IPackageManager mPackageManager;
- protected SettingsObserver mSettingsObserver;
+
+ @VisibleForTesting
+ protected AssistantSettings.Factory mSettingsFactory = AssistantSettings.FACTORY;
+ @VisibleForTesting
+ protected AssistantSettings mSettings;
public Assistant() {
}
@@ -118,7 +115,8 @@
// Contexts are correctly hooked up by the creation step, which is required for the observer
// to be hooked up/initialized.
mPackageManager = ActivityThread.getPackageManager();
- mSettingsObserver = new SettingsObserver(mHandler);
+ mSettings = mSettingsFactory.createAndRegister(mHandler,
+ getApplicationContext().getContentResolver(), getUserId(), this::updateThresholds);
mSmartActionsHelper = new SmartActionsHelper();
mNotificationCategorizer = new NotificationCategorizer();
mAgingHelper = new AgingHelper(getContext(),
@@ -216,11 +214,11 @@
if (!isForCurrentUser(sbn)) {
return null;
}
- NotificationEntry entry = new NotificationEntry(
- ActivityThread.getPackageManager(), sbn, channel);
+ NotificationEntry entry = new NotificationEntry(mPackageManager, sbn, channel);
ArrayList<Notification.Action> actions =
- mSmartActionsHelper.suggestActions(this, entry);
- ArrayList<CharSequence> replies = mSmartActionsHelper.suggestReplies(this, entry);
+ mSmartActionsHelper.suggestActions(this, entry, mSettings);
+ ArrayList<CharSequence> replies =
+ mSmartActionsHelper.suggestReplies(this, entry, mSettings);
return createEnqueuedNotificationAdjustment(entry, actions, replies);
}
@@ -239,8 +237,7 @@
if (!smartReplies.isEmpty()) {
signals.putCharSequenceArrayList(Adjustment.KEY_SMART_REPLIES, smartReplies);
}
- if (Settings.Secure.getInt(getContentResolver(),
- Settings.Secure.NOTIFICATION_NEW_INTERRUPTION_MODEL, 1) == 1) {
+ if (mSettings.mNewInterruptionModel) {
if (mNotificationCategorizer.shouldSilence(entry)) {
final int importance = entry.getImportance() < IMPORTANCE_LOW
? entry.getImportance() : IMPORTANCE_LOW;
@@ -460,6 +457,11 @@
}
@VisibleForTesting
+ public void setSmartActionsHelper(SmartActionsHelper smartActionsHelper) {
+ mSmartActionsHelper = smartActionsHelper;
+ }
+
+ @VisibleForTesting
public ChannelImpressions getImpressions(String key) {
synchronized (mkeyToImpressions) {
return mkeyToImpressions.get(key);
@@ -475,10 +477,20 @@
private ChannelImpressions createChannelImpressionsWithThresholds() {
ChannelImpressions impressions = new ChannelImpressions();
- impressions.updateThresholds(mDismissToViewRatioLimit, mStreakLimit);
+ impressions.updateThresholds(mSettings.mDismissToViewRatioLimit, mSettings.mStreakLimit);
return impressions;
}
+ private void updateThresholds() {
+ // Update all existing channel impression objects with any new limits/thresholds.
+ synchronized (mkeyToImpressions) {
+ for (ChannelImpressions channelImpressions: mkeyToImpressions.values()) {
+ channelImpressions.updateThresholds(
+ mSettings.mDismissToViewRatioLimit, mSettings.mStreakLimit);
+ }
+ }
+ }
+
protected final class AgingCallback implements Callback {
@Override
public void sendAdjustment(String key, int newImportance) {
@@ -495,51 +507,4 @@
}
}
- /**
- * Observer for updates on blocking helper threshold values.
- */
- protected final class SettingsObserver extends ContentObserver {
- private final Uri STREAK_LIMIT_URI =
- Settings.Global.getUriFor(Settings.Global.BLOCKING_HELPER_STREAK_LIMIT);
- private final Uri DISMISS_TO_VIEW_RATIO_LIMIT_URI =
- Settings.Global.getUriFor(
- Settings.Global.BLOCKING_HELPER_DISMISS_TO_VIEW_RATIO_LIMIT);
-
- public SettingsObserver(Handler handler) {
- super(handler);
- ContentResolver resolver = getApplicationContext().getContentResolver();
- resolver.registerContentObserver(
- DISMISS_TO_VIEW_RATIO_LIMIT_URI, false, this, getUserId());
- resolver.registerContentObserver(STREAK_LIMIT_URI, false, this, getUserId());
-
- // Update all uris on creation.
- update(null);
- }
-
- @Override
- public void onChange(boolean selfChange, Uri uri) {
- update(uri);
- }
-
- private void update(Uri uri) {
- ContentResolver resolver = getApplicationContext().getContentResolver();
- if (uri == null || DISMISS_TO_VIEW_RATIO_LIMIT_URI.equals(uri)) {
- mDismissToViewRatioLimit = Settings.Global.getFloat(
- resolver, Settings.Global.BLOCKING_HELPER_DISMISS_TO_VIEW_RATIO_LIMIT,
- ChannelImpressions.DEFAULT_DISMISS_TO_VIEW_RATIO_LIMIT);
- }
- if (uri == null || STREAK_LIMIT_URI.equals(uri)) {
- mStreakLimit = Settings.Global.getInt(
- resolver, Settings.Global.BLOCKING_HELPER_STREAK_LIMIT,
- ChannelImpressions.DEFAULT_STREAK_LIMIT);
- }
-
- // Update all existing channel impression objects with any new limits/thresholds.
- synchronized (mkeyToImpressions) {
- for (ChannelImpressions channelImpressions: mkeyToImpressions.values()) {
- channelImpressions.updateThresholds(mDismissToViewRatioLimit, mStreakLimit);
- }
- }
- }
- }
}
diff --git a/packages/ExtServices/src/android/ext/services/notification/AssistantSettings.java b/packages/ExtServices/src/android/ext/services/notification/AssistantSettings.java
new file mode 100644
index 0000000..39a1676
--- /dev/null
+++ b/packages/ExtServices/src/android/ext/services/notification/AssistantSettings.java
@@ -0,0 +1,140 @@
+/**
+ * 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 android.ext.services.notification;
+
+import android.content.ContentResolver;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+import android.provider.Settings;
+import android.util.KeyValueListParser;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * Observes the settings for {@link Assistant}.
+ */
+final class AssistantSettings extends ContentObserver {
+ public static Factory FACTORY = AssistantSettings::createAndRegister;
+ private static final boolean DEFAULT_GENERATE_REPLIES = true;
+ private static final boolean DEFAULT_GENERATE_ACTIONS = true;
+ private static final int DEFAULT_NEW_INTERRUPTION_MODEL_INT = 1;
+
+ private static final Uri STREAK_LIMIT_URI =
+ Settings.Global.getUriFor(Settings.Global.BLOCKING_HELPER_STREAK_LIMIT);
+ private static final Uri DISMISS_TO_VIEW_RATIO_LIMIT_URI =
+ Settings.Global.getUriFor(
+ Settings.Global.BLOCKING_HELPER_DISMISS_TO_VIEW_RATIO_LIMIT);
+ private static final Uri SMART_SUGGESTIONS_IN_NOTIFICATIONS_FLAGS_URI =
+ Settings.Global.getUriFor(
+ Settings.Global.SMART_SUGGESTIONS_IN_NOTIFICATIONS_FLAGS);
+ private static final Uri NOTIFICATION_NEW_INTERRUPTION_MODEL_URI =
+ Settings.Secure.getUriFor(Settings.Secure.NOTIFICATION_NEW_INTERRUPTION_MODEL);
+
+ private static final String KEY_GENERATE_REPLIES = "generate_replies";
+ private static final String KEY_GENERATE_ACTIONS = "generate_actions";
+
+ private final KeyValueListParser mParser = new KeyValueListParser(',');
+ private final ContentResolver mResolver;
+ private final int mUserId;
+
+ @VisibleForTesting
+ protected final Runnable mOnUpdateRunnable;
+
+ // Actuall configuration settings.
+ float mDismissToViewRatioLimit;
+ int mStreakLimit;
+ boolean mGenerateReplies = DEFAULT_GENERATE_REPLIES;
+ boolean mGenerateActions = DEFAULT_GENERATE_ACTIONS;
+ boolean mNewInterruptionModel;
+
+ private AssistantSettings(Handler handler, ContentResolver resolver, int userId,
+ Runnable onUpdateRunnable) {
+ super(handler);
+ mResolver = resolver;
+ mUserId = userId;
+ mOnUpdateRunnable = onUpdateRunnable;
+ }
+
+ private static AssistantSettings createAndRegister(
+ Handler handler, ContentResolver resolver, int userId, Runnable onUpdateRunnable) {
+ AssistantSettings assistantSettings =
+ new AssistantSettings(handler, resolver, userId, onUpdateRunnable);
+ assistantSettings.register();
+ return assistantSettings;
+ }
+
+ /**
+ * Creates an instance but doesn't register it as an observer.
+ */
+ @VisibleForTesting
+ protected static AssistantSettings createForTesting(
+ Handler handler, ContentResolver resolver, int userId, Runnable onUpdateRunnable) {
+ return new AssistantSettings(handler, resolver, userId, onUpdateRunnable);
+ }
+
+ private void register() {
+ mResolver.registerContentObserver(
+ DISMISS_TO_VIEW_RATIO_LIMIT_URI, false, this, mUserId);
+ mResolver.registerContentObserver(STREAK_LIMIT_URI, false, this, mUserId);
+ mResolver.registerContentObserver(
+ SMART_SUGGESTIONS_IN_NOTIFICATIONS_FLAGS_URI, false, this, mUserId);
+
+ // Update all uris on creation.
+ update(null);
+ }
+
+ @Override
+ public void onChange(boolean selfChange, Uri uri) {
+ update(uri);
+ }
+
+ private void update(Uri uri) {
+ if (uri == null || DISMISS_TO_VIEW_RATIO_LIMIT_URI.equals(uri)) {
+ mDismissToViewRatioLimit = Settings.Global.getFloat(
+ mResolver, Settings.Global.BLOCKING_HELPER_DISMISS_TO_VIEW_RATIO_LIMIT,
+ ChannelImpressions.DEFAULT_DISMISS_TO_VIEW_RATIO_LIMIT);
+ }
+ if (uri == null || STREAK_LIMIT_URI.equals(uri)) {
+ mStreakLimit = Settings.Global.getInt(
+ mResolver, Settings.Global.BLOCKING_HELPER_STREAK_LIMIT,
+ ChannelImpressions.DEFAULT_STREAK_LIMIT);
+ }
+ if (uri == null || SMART_SUGGESTIONS_IN_NOTIFICATIONS_FLAGS_URI.equals(uri)) {
+ mParser.setString(
+ Settings.Global.getString(mResolver,
+ Settings.Global.SMART_SUGGESTIONS_IN_NOTIFICATIONS_FLAGS));
+ mGenerateReplies =
+ mParser.getBoolean(KEY_GENERATE_REPLIES, DEFAULT_GENERATE_REPLIES);
+ mGenerateActions =
+ mParser.getBoolean(KEY_GENERATE_ACTIONS, DEFAULT_GENERATE_ACTIONS);
+ }
+ if (uri == null || NOTIFICATION_NEW_INTERRUPTION_MODEL_URI.equals(uri)) {
+ int mNewInterruptionModelInt = Settings.Secure.getInt(
+ mResolver, Settings.Secure.NOTIFICATION_NEW_INTERRUPTION_MODEL,
+ DEFAULT_NEW_INTERRUPTION_MODEL_INT);
+ mNewInterruptionModel = mNewInterruptionModelInt == 1;
+ }
+
+ mOnUpdateRunnable.run();
+ }
+
+ public interface Factory {
+ AssistantSettings createAndRegister(Handler handler, ContentResolver resolver, int userId,
+ Runnable onUpdateRunnable);
+ }
+}
diff --git a/packages/ExtServices/src/android/ext/services/notification/SmartActionsHelper.java b/packages/ExtServices/src/android/ext/services/notification/SmartActionsHelper.java
index 892267b..6f2b6c9 100644
--- a/packages/ExtServices/src/android/ext/services/notification/SmartActionsHelper.java
+++ b/packages/ExtServices/src/android/ext/services/notification/SmartActionsHelper.java
@@ -69,8 +69,11 @@
* from notification text / message, we can replace most of the code here by consuming that API.
*/
@NonNull
- ArrayList<Notification.Action> suggestActions(
- @Nullable Context context, @NonNull NotificationEntry entry) {
+ ArrayList<Notification.Action> suggestActions(@Nullable Context context,
+ @NonNull NotificationEntry entry, @NonNull AssistantSettings settings) {
+ if (!settings.mGenerateActions) {
+ return EMPTY_ACTION_LIST;
+ }
if (!isEligibleForActionAdjustment(entry)) {
return EMPTY_ACTION_LIST;
}
@@ -86,8 +89,11 @@
getMostSalientActionText(entry.getNotification()), MAX_SMART_ACTIONS);
}
- ArrayList<CharSequence> suggestReplies(
- @Nullable Context context, @NonNull NotificationEntry entry) {
+ ArrayList<CharSequence> suggestReplies(@Nullable Context context,
+ @NonNull NotificationEntry entry, @NonNull AssistantSettings settings) {
+ if (!settings.mGenerateReplies) {
+ return EMPTY_REPLY_LIST;
+ }
if (!isEligibleForReplyAdjustment(entry)) {
return EMPTY_REPLY_LIST;
}
diff --git a/packages/ExtServices/tests/src/android/ext/services/notification/AssistantSettingsTest.java b/packages/ExtServices/tests/src/android/ext/services/notification/AssistantSettingsTest.java
new file mode 100644
index 0000000..fd23f2b
--- /dev/null
+++ b/packages/ExtServices/tests/src/android/ext/services/notification/AssistantSettingsTest.java
@@ -0,0 +1,162 @@
+/**
+ * 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 android.ext.services.notification;
+
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import android.content.ContentResolver;
+import android.os.Handler;
+import android.os.Looper;
+import android.provider.Settings;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.runner.AndroidJUnit4;
+import android.testing.TestableContext;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidJUnit4.class)
+public class AssistantSettingsTest {
+ private static final int USER_ID = 5;
+
+ @Rule
+ public final TestableContext mContext =
+ new TestableContext(InstrumentationRegistry.getContext(), null);
+
+ @Mock Runnable mOnUpdateRunnable;
+
+ private ContentResolver mResolver;
+ private AssistantSettings mAssistantSettings;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ mResolver = mContext.getContentResolver();
+ Handler handler = new Handler(Looper.getMainLooper());
+
+ // To bypass real calls to global settings values, set the Settings values here.
+ Settings.Global.putFloat(mResolver,
+ Settings.Global.BLOCKING_HELPER_DISMISS_TO_VIEW_RATIO_LIMIT, 0.8f);
+ Settings.Global.putInt(mResolver, Settings.Global.BLOCKING_HELPER_STREAK_LIMIT, 2);
+ Settings.Global.putString(mResolver,
+ Settings.Global.SMART_SUGGESTIONS_IN_NOTIFICATIONS_FLAGS,
+ "generate_replies=true,generate_actions=true");
+ Settings.Secure.putInt(mResolver, Settings.Secure.NOTIFICATION_NEW_INTERRUPTION_MODEL, 1);
+
+ mAssistantSettings = AssistantSettings.createForTesting(
+ handler, mResolver, USER_ID, mOnUpdateRunnable);
+ }
+
+ @Test
+ public void testGenerateRepliesDisabled() {
+ Settings.Global.putString(mResolver,
+ Settings.Global.SMART_SUGGESTIONS_IN_NOTIFICATIONS_FLAGS,
+ "generate_replies=false");
+
+ // Notify for the settings values we updated.
+ mAssistantSettings.onChange(false,
+ Settings.Global.getUriFor(
+ Settings.Global.SMART_SUGGESTIONS_IN_NOTIFICATIONS_FLAGS));
+
+
+ assertFalse(mAssistantSettings.mGenerateReplies);
+ }
+
+ @Test
+ public void testGenerateRepliesEnabled() {
+ Settings.Global.putString(mResolver,
+ Settings.Global.SMART_SUGGESTIONS_IN_NOTIFICATIONS_FLAGS, "generate_replies=true");
+
+ // Notify for the settings values we updated.
+ mAssistantSettings.onChange(false,
+ Settings.Global.getUriFor(
+ Settings.Global.SMART_SUGGESTIONS_IN_NOTIFICATIONS_FLAGS));
+
+ assertTrue(mAssistantSettings.mGenerateReplies);
+ }
+
+ @Test
+ public void testGenerateActionsDisabled() {
+ Settings.Global.putString(mResolver,
+ Settings.Global.SMART_SUGGESTIONS_IN_NOTIFICATIONS_FLAGS, "generate_actions=false");
+
+ // Notify for the settings values we updated.
+ mAssistantSettings.onChange(false,
+ Settings.Global.getUriFor(
+ Settings.Global.SMART_SUGGESTIONS_IN_NOTIFICATIONS_FLAGS));
+
+ assertTrue(mAssistantSettings.mGenerateReplies);
+ }
+
+ @Test
+ public void testGenerateActionsEnabled() {
+ Settings.Global.putString(mResolver,
+ Settings.Global.SMART_SUGGESTIONS_IN_NOTIFICATIONS_FLAGS, "generate_actions=true");
+
+ // Notify for the settings values we updated.
+ mAssistantSettings.onChange(false,
+ Settings.Global.getUriFor(
+ Settings.Global.SMART_SUGGESTIONS_IN_NOTIFICATIONS_FLAGS));
+
+ assertTrue(mAssistantSettings.mGenerateReplies);
+ }
+
+ @Test
+ public void testStreakLimit() {
+ verify(mOnUpdateRunnable, never()).run();
+
+ // Update settings value.
+ int newStreakLimit = 4;
+ Settings.Global.putInt(mResolver,
+ Settings.Global.BLOCKING_HELPER_STREAK_LIMIT, newStreakLimit);
+
+ // Notify for the settings value we updated.
+ mAssistantSettings.onChange(false, Settings.Global.getUriFor(
+ Settings.Global.BLOCKING_HELPER_STREAK_LIMIT));
+
+ assertEquals(newStreakLimit, mAssistantSettings.mStreakLimit);
+ verify(mOnUpdateRunnable).run();
+ }
+
+ @Test
+ public void testDismissToViewRatioLimit() {
+ verify(mOnUpdateRunnable, never()).run();
+
+ // Update settings value.
+ float newDismissToViewRatioLimit = 3f;
+ Settings.Global.putFloat(mResolver,
+ Settings.Global.BLOCKING_HELPER_DISMISS_TO_VIEW_RATIO_LIMIT,
+ newDismissToViewRatioLimit);
+
+ // Notify for the settings value we updated.
+ mAssistantSettings.onChange(false, Settings.Global.getUriFor(
+ Settings.Global.BLOCKING_HELPER_DISMISS_TO_VIEW_RATIO_LIMIT));
+
+ assertEquals(newDismissToViewRatioLimit, mAssistantSettings.mDismissToViewRatioLimit, 1e-6);
+ verify(mOnUpdateRunnable).run();
+ }
+}
diff --git a/packages/ExtServices/tests/src/android/ext/services/notification/AssistantTest.java b/packages/ExtServices/tests/src/android/ext/services/notification/AssistantTest.java
index 2eb005a..0a95b83 100644
--- a/packages/ExtServices/tests/src/android/ext/services/notification/AssistantTest.java
+++ b/packages/ExtServices/tests/src/android/ext/services/notification/AssistantTest.java
@@ -33,13 +33,11 @@
import android.app.INotificationManager;
import android.app.Notification;
import android.app.NotificationChannel;
-import android.content.ContentResolver;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.IPackageManager;
import android.os.Build;
import android.os.UserHandle;
-import android.provider.Settings;
import android.service.notification.Adjustment;
import android.service.notification.NotificationListenerService;
import android.service.notification.NotificationListenerService.Ranking;
@@ -86,8 +84,7 @@
@Mock INotificationManager mNoMan;
@Mock AtomicFile mFile;
- @Mock
- IPackageManager mPackageManager;
+ @Mock IPackageManager mPackageManager;
Assistant mAssistant;
Application mApplication;
@@ -108,20 +105,26 @@
new Intent("android.service.notification.NotificationAssistantService");
startIntent.setPackage("android.ext.services");
- // To bypass real calls to global settings values, set the Settings values here.
- Settings.Global.putFloat(mContext.getContentResolver(),
- Settings.Global.BLOCKING_HELPER_DISMISS_TO_VIEW_RATIO_LIMIT, 0.8f);
- Settings.Global.putInt(mContext.getContentResolver(),
- Settings.Global.BLOCKING_HELPER_STREAK_LIMIT, 2);
mApplication = (Application) InstrumentationRegistry.getInstrumentation().
getTargetContext().getApplicationContext();
// Force the test to use the correct application instead of trying to use a mock application
setApplication(mApplication);
- bindService(startIntent);
+
+ setupService();
mAssistant = getService();
+
+ // Override the AssistantSettings factory.
+ mAssistant.mSettingsFactory = AssistantSettings::createForTesting;
+
+ bindService(startIntent);
+
+ mAssistant.mSettings.mDismissToViewRatioLimit = 0.8f;
+ mAssistant.mSettings.mStreakLimit = 2;
+ mAssistant.mSettings.mNewInterruptionModel = true;
mAssistant.setNoMan(mNoMan);
mAssistant.setFile(mFile);
mAssistant.setPackageManager(mPackageManager);
+
ApplicationInfo info = mock(ApplicationInfo.class);
when(mPackageManager.getApplicationInfo(anyString(), anyInt(), anyInt()))
.thenReturn(info);
@@ -408,6 +411,8 @@
mAssistant.writeXml(serializer);
Assistant assistant = new Assistant();
+ // onCreate is not invoked, so settings won't be initialised, unless we do it here.
+ assistant.mSettings = mAssistant.mSettings;
assistant.readXml(new BufferedInputStream(new ByteArrayInputStream(baos.toByteArray())));
assertEquals(ci1, assistant.getImpressions(key1));
@@ -417,8 +422,6 @@
@Test
public void testSettingsProviderUpdate() {
- ContentResolver resolver = mApplication.getContentResolver();
-
// Set up channels
String key = mAssistant.getKey("pkg1", 1, "channel1");
ChannelImpressions ci = new ChannelImpressions();
@@ -435,19 +438,11 @@
assertEquals(false, ci.shouldTriggerBlock());
// Update settings values.
- float newDismissToViewRatioLimit = 0f;
- int newStreakLimit = 0;
- Settings.Global.putFloat(resolver,
- Settings.Global.BLOCKING_HELPER_DISMISS_TO_VIEW_RATIO_LIMIT,
- newDismissToViewRatioLimit);
- Settings.Global.putInt(resolver,
- Settings.Global.BLOCKING_HELPER_STREAK_LIMIT, newStreakLimit);
+ mAssistant.mSettings.mDismissToViewRatioLimit = 0f;
+ mAssistant.mSettings.mStreakLimit = 0;
// Notify for the settings values we updated.
- mAssistant.mSettingsObserver.onChange(false, Settings.Global.getUriFor(
- Settings.Global.BLOCKING_HELPER_STREAK_LIMIT));
- mAssistant.mSettingsObserver.onChange(false, Settings.Global.getUriFor(
- Settings.Global.BLOCKING_HELPER_DISMISS_TO_VIEW_RATIO_LIMIT));
+ mAssistant.mSettings.mOnUpdateRunnable.run();
// With the new threshold, the blocking helper should be triggered.
assertEquals(true, ci.shouldTriggerBlock());
diff --git a/packages/SettingsLib/Android.bp b/packages/SettingsLib/Android.bp
index cc17b25..0126e7e5 100644
--- a/packages/SettingsLib/Android.bp
+++ b/packages/SettingsLib/Android.bp
@@ -18,6 +18,7 @@
"SettingsLibSettingsSpinner",
"SettingsLayoutPreference",
"ActionButtonsPreference",
+ "SettingsLibEntityHeaderWidgets",
],
// ANDROIDMK TRANSLATION ERROR: unsupported assignment to LOCAL_SHARED_JAVA_LIBRARIES
diff --git a/packages/SettingsLib/EntityHeaderWidgets/Android.bp b/packages/SettingsLib/EntityHeaderWidgets/Android.bp
new file mode 100644
index 0000000..3ca4ecd
--- /dev/null
+++ b/packages/SettingsLib/EntityHeaderWidgets/Android.bp
@@ -0,0 +1,14 @@
+android_library {
+ name: "SettingsLibEntityHeaderWidgets",
+
+ srcs: ["src/**/*.java"],
+ resource_dirs: ["res"],
+
+ static_libs: [
+ "androidx.annotation_annotation",
+ "SettingsLibAppPreference"
+ ],
+
+ sdk_version: "system_current",
+ min_sdk_version: "21",
+}
diff --git a/packages/SettingsLib/EntityHeaderWidgets/AndroidManifest.xml b/packages/SettingsLib/EntityHeaderWidgets/AndroidManifest.xml
new file mode 100644
index 0000000..4b9f1ab
--- /dev/null
+++ b/packages/SettingsLib/EntityHeaderWidgets/AndroidManifest.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.settingslib.widget">
+
+ <uses-sdk android:minSdkVersion="21" />
+
+</manifest>
diff --git a/packages/SettingsLib/EntityHeaderWidgets/res/layout/app_entities_header.xml b/packages/SettingsLib/EntityHeaderWidgets/res/layout/app_entities_header.xml
new file mode 100644
index 0000000..9f30eda
--- /dev/null
+++ b/packages/SettingsLib/EntityHeaderWidgets/res/layout/app_entities_header.xml
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+ -->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/app_entities_header"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingStart="24dp"
+ android:paddingEnd="8dp"
+ android:gravity="center"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/header_title"
+ android:layout_width="wrap_content"
+ android:layout_height="48dp"
+ android:gravity="center"
+ android:textAppearance="@style/AppEntitiesHeader.Text.HeaderTitle"/>
+
+ <LinearLayout
+ android:id="@+id/all_apps_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="16dp"
+ android:paddingBottom="16dp"
+ android:gravity="center">
+
+ <include
+ android:id="@+id/app1_view"
+ layout="@layout/app_view"/>
+
+ <include
+ android:id="@+id/app2_view"
+ layout="@layout/app_view"/>
+
+ <include
+ android:id="@+id/app3_view"
+ layout="@layout/app_view"/>
+
+ </LinearLayout>
+
+ <Button
+ android:id="@+id/header_details"
+ style="@*android:style/Widget.DeviceDefault.Button.Borderless.Colored"
+ android:layout_width="wrap_content"
+ android:layout_height="48dp"
+ android:gravity="center"/>
+
+</LinearLayout>
diff --git a/packages/SettingsLib/EntityHeaderWidgets/res/layout/app_view.xml b/packages/SettingsLib/EntityHeaderWidgets/res/layout/app_view.xml
new file mode 100644
index 0000000..fcafa31
--- /dev/null
+++ b/packages/SettingsLib/EntityHeaderWidgets/res/layout/app_view.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+ -->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginEnd="16dp"
+ android:gravity="center"
+ android:orientation="vertical">
+
+ <ImageView
+ android:id="@+id/app_icon"
+ android:layout_width="@dimen/secondary_app_icon_size"
+ android:layout_height="@dimen/secondary_app_icon_size"
+ android:layout_marginBottom="12dp"/>
+
+ <TextView
+ android:id="@+id/app_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="2dp"
+ android:singleLine="true"
+ android:ellipsize="marquee"
+ android:textAppearance="@style/AppEntitiesHeader.Text.Title"/>
+
+ <TextView
+ android:id="@+id/app_summary"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:ellipsize="marquee"
+ android:textAppearance="@style/AppEntitiesHeader.Text.Summary"/>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/packages/SettingsLib/EntityHeaderWidgets/res/values/styles.xml b/packages/SettingsLib/EntityHeaderWidgets/res/values/styles.xml
new file mode 100644
index 0000000..0eefd4b
--- /dev/null
+++ b/packages/SettingsLib/EntityHeaderWidgets/res/values/styles.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+ -->
+
+<resources>
+ <style name="AppEntitiesHeader.Text"
+ parent="@android:style/TextAppearance.Material.Subhead">
+ <item name="android:fontFamily">@*android:string/config_headlineFontFamilyMedium</item>
+ <item name="android:textColor">?android:attr/textColorPrimary</item>
+ </style>
+
+ <style name="AppEntitiesHeader.Text.HeaderTitle">
+ <item name="android:textSize">14sp</item>
+ </style>
+
+ <style name="AppEntitiesHeader.Text.Title">
+ <item name="android:textSize">16sp</item>
+ </style>
+
+ <style name="AppEntitiesHeader.Text.Summary"
+ parent="@android:style/TextAppearance.Material.Body1">
+ <item name="android:textColor">?android:attr/textColorSecondary</item>
+ <item name="android:textSize">14sp</item>
+ </style>
+</resources>
\ No newline at end of file
diff --git a/packages/SettingsLib/EntityHeaderWidgets/src/com/android/settingslib/widget/AppEntitiesHeaderController.java b/packages/SettingsLib/EntityHeaderWidgets/src/com/android/settingslib/widget/AppEntitiesHeaderController.java
new file mode 100644
index 0000000..8ccf89f
--- /dev/null
+++ b/packages/SettingsLib/EntityHeaderWidgets/src/com/android/settingslib/widget/AppEntitiesHeaderController.java
@@ -0,0 +1,267 @@
+/*
+ * 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.settingslib.widget;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.View;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+import androidx.annotation.VisibleForTesting;
+
+/**
+ * This is used to initialize view which was inflated
+ * from {@link R.xml.app_entities_header.xml}.
+ *
+ * <p>The view looks like below.
+ *
+ * <pre>
+ * --------------------------------------------------------------
+ * | Header title |
+ * --------------------------------------------------------------
+ * | App1 icon | App2 icon | App3 icon |
+ * | App1 title | App2 title | App3 title |
+ * | App1 summary | App2 summary | App3 summary |
+ * |-------------------------------------------------------------
+ * | Header details |
+ * --------------------------------------------------------------
+ * </pre>
+ *
+ * <p>How to use AppEntitiesHeaderController?
+ *
+ * <p>1. Add a {@link LayoutPreference} in layout XML file.
+ * <pre>
+ * <com.android.settingslib.widget.LayoutPreference
+ * android:key="app_entities_header"
+ * android:layout="@layout/app_entities_header"/>
+ * </pre>
+ *
+ * <p>2. Use AppEntitiesHeaderController to call below methods, then you can initialize
+ * view of <code>app_entities_header</code>.
+ *
+ * <pre>
+ *
+ * View headerView = ((LayoutPreference) screen.findPreference("app_entities_header"))
+ * .findViewById(R.id.app_entities_header);
+ *
+ * AppEntitiesHeaderController.newInstance(context, headerView)
+ * .setHeaderTitleRes(R.string.xxxxx)
+ * .setHeaderDetailsRes(R.string.xxxxx)
+ * .setHeaderDetailsClickListener(onClickListener)
+ * .setAppEntity(0, icon, "app title", "app summary")
+ * .setAppEntity(1, icon, "app title", "app summary")
+ * .setAppEntity(2, icon, "app title", "app summary")
+ * .apply();
+ * </pre>
+ */
+public class AppEntitiesHeaderController {
+
+ private static final String TAG = "AppEntitiesHeaderCtl";
+
+ @VisibleForTesting
+ static final int MAXIMUM_APPS = 3;
+
+ private final Context mContext;
+ private final TextView mHeaderTitleView;
+ private final Button mHeaderDetailsView;
+
+ private final AppEntity[] mAppEntities;
+ private final View[] mAppEntityViews;
+ private final ImageView[] mAppIconViews;
+ private final TextView[] mAppTitleViews;
+ private final TextView[] mAppSummaryViews;
+
+ private int mHeaderTitleRes;
+ private int mHeaderDetailsRes;
+ private View.OnClickListener mDetailsOnClickListener;
+
+ /**
+ * Creates a new instance of the controller.
+ *
+ * @param context the Context the view is running in
+ * @param appEntitiesHeaderView view was inflated from <code>app_entities_header</code>
+ */
+ public static AppEntitiesHeaderController newInstance(@NonNull Context context,
+ @NonNull View appEntitiesHeaderView) {
+ return new AppEntitiesHeaderController(context, appEntitiesHeaderView);
+ }
+
+ private AppEntitiesHeaderController(Context context, View appEntitiesHeaderView) {
+ mContext = context;
+ mHeaderTitleView = appEntitiesHeaderView.findViewById(R.id.header_title);
+ mHeaderDetailsView = appEntitiesHeaderView.findViewById(R.id.header_details);
+
+ mAppEntities = new AppEntity[MAXIMUM_APPS];
+ mAppIconViews = new ImageView[MAXIMUM_APPS];
+ mAppTitleViews = new TextView[MAXIMUM_APPS];
+ mAppSummaryViews = new TextView[MAXIMUM_APPS];
+
+ mAppEntityViews = new View[]{
+ appEntitiesHeaderView.findViewById(R.id.app1_view),
+ appEntitiesHeaderView.findViewById(R.id.app2_view),
+ appEntitiesHeaderView.findViewById(R.id.app3_view)
+ };
+
+ // Initialize view in advance, so we won't take too much time to do it when controller is
+ // binding view.
+ for (int index = 0; index < MAXIMUM_APPS; index++) {
+ final View appView = mAppEntityViews[index];
+ mAppIconViews[index] = (ImageView) appView.findViewById(R.id.app_icon);
+ mAppTitleViews[index] = (TextView) appView.findViewById(R.id.app_title);
+ mAppSummaryViews[index] = (TextView) appView.findViewById(R.id.app_summary);
+ }
+ }
+
+ /**
+ * Set the text resource for app entities header title.
+ */
+ public AppEntitiesHeaderController setHeaderTitleRes(@StringRes int titleRes) {
+ mHeaderTitleRes = titleRes;
+ return this;
+ }
+
+ /**
+ * Set the text resource for app entities header details.
+ */
+ public AppEntitiesHeaderController setHeaderDetailsRes(@StringRes int detailsRes) {
+ mHeaderDetailsRes = detailsRes;
+ return this;
+ }
+
+ /**
+ * Register a callback to be invoked when header details view is clicked.
+ */
+ public AppEntitiesHeaderController setHeaderDetailsClickListener(
+ @Nullable View.OnClickListener clickListener) {
+ mDetailsOnClickListener = clickListener;
+ return this;
+ }
+
+ /**
+ * Set an app entity at a specified position view.
+ *
+ * @param index the index at which the specified view is to be inserted
+ * @param icon the icon of app entity
+ * @param titleRes the title of app entity
+ * @param summaryRes the summary of app entity
+ * @return this {@code AppEntitiesHeaderController} object
+ */
+ public AppEntitiesHeaderController setAppEntity(int index, @NonNull Drawable icon,
+ @Nullable CharSequence titleRes, @Nullable CharSequence summaryRes) {
+ final AppEntity appEntity = new AppEntity(icon, titleRes, summaryRes);
+ mAppEntities[index] = appEntity;
+ return this;
+ }
+
+ /**
+ * Remove an app entity at a specified position view.
+ *
+ * @param index the index at which the specified view is to be removed
+ * @return this {@code AppEntitiesHeaderController} object
+ */
+ public AppEntitiesHeaderController removeAppEntity(int index) {
+ mAppEntities[index] = null;
+ return this;
+ }
+
+ /**
+ * Clear all app entities in app entities header.
+ *
+ * @return this {@code AppEntitiesHeaderController} object
+ */
+ public AppEntitiesHeaderController clearAllAppEntities() {
+ for (int index = 0; index < MAXIMUM_APPS; index++) {
+ removeAppEntity(index);
+ }
+ return this;
+ }
+
+ /**
+ * Done mutating app entities header, rebinds everything.
+ */
+ public void apply() {
+ bindHeaderTitleView();
+ bindHeaderDetailsView();
+
+ // Rebind all apps view
+ for (int index = 0; index < MAXIMUM_APPS; index++) {
+ bindAppEntityView(index);
+ }
+ }
+
+ private void bindHeaderTitleView() {
+ CharSequence titleText = "";
+ try {
+ titleText = mContext.getText(mHeaderTitleRes);
+ } catch (Resources.NotFoundException e) {
+ Log.e(TAG, "Resource of header title can't not be found!", e);
+ }
+ mHeaderTitleView.setText(titleText);
+ mHeaderTitleView.setVisibility(
+ TextUtils.isEmpty(titleText) ? View.GONE : View.VISIBLE);
+ }
+
+ private void bindHeaderDetailsView() {
+ CharSequence detailsText = "";
+ try {
+ detailsText = mContext.getText(mHeaderDetailsRes);
+ } catch (Resources.NotFoundException e) {
+ Log.e(TAG, "Resource of header details can't not be found!", e);
+ }
+ mHeaderDetailsView.setText(detailsText);
+ mHeaderDetailsView.setVisibility(
+ TextUtils.isEmpty(detailsText) ? View.GONE : View.VISIBLE);
+ mHeaderDetailsView.setOnClickListener(mDetailsOnClickListener);
+ }
+
+ private void bindAppEntityView(int index) {
+ final AppEntity appEntity = mAppEntities[index];
+ mAppEntityViews[index].setVisibility(appEntity != null ? View.VISIBLE : View.GONE);
+
+ if (appEntity != null) {
+ mAppIconViews[index].setImageDrawable(appEntity.icon);
+
+ mAppTitleViews[index].setVisibility(
+ TextUtils.isEmpty(appEntity.title) ? View.INVISIBLE : View.VISIBLE);
+ mAppTitleViews[index].setText(appEntity.title);
+
+ mAppSummaryViews[index].setVisibility(
+ TextUtils.isEmpty(appEntity.summary) ? View.INVISIBLE : View.VISIBLE);
+ mAppSummaryViews[index].setText(appEntity.summary);
+ }
+ }
+
+ private static class AppEntity {
+ public final Drawable icon;
+ public final CharSequence title;
+ public final CharSequence summary;
+
+ AppEntity(Drawable appIcon, CharSequence appTitle, CharSequence appSummary) {
+ icon = appIcon;
+ title = appTitle;
+ summary = appSummary;
+ }
+ }
+}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/AppEntitiesHeaderControllerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/AppEntitiesHeaderControllerTest.java
new file mode 100644
index 0000000..c3bc8da
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/AppEntitiesHeaderControllerTest.java
@@ -0,0 +1,181 @@
+/*
+ * 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.settingslib.widget;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+@RunWith(RobolectricTestRunner.class)
+public class AppEntitiesHeaderControllerTest {
+
+ private static final CharSequence TITLE = "APP_TITLE";
+ private static final CharSequence SUMMARY = "APP_SUMMARY";
+
+ @Rule
+ public final ExpectedException thrown = ExpectedException.none();
+
+ private Context mContext;
+ private Drawable mIcon;
+ private View mAppEntitiesHeaderView;
+ private AppEntitiesHeaderController mController;
+
+ @Before
+ public void setUp() {
+ mContext = RuntimeEnvironment.application;
+ mAppEntitiesHeaderView = LayoutInflater.from(mContext).inflate(
+ R.layout.app_entities_header, null /* root */);
+ mIcon = mContext.getDrawable(R.drawable.ic_menu);
+ mController = AppEntitiesHeaderController.newInstance(mContext,
+ mAppEntitiesHeaderView);
+ }
+
+ @Test
+ public void assert_amountOfMaximumAppsAreThree() {
+ assertThat(AppEntitiesHeaderController.MAXIMUM_APPS).isEqualTo(3);
+ }
+
+ @Test
+ public void setHeaderTitleRes_setTextRes_shouldSetToTitleView() {
+ mController.setHeaderTitleRes(R.string.expand_button_title).apply();
+ final TextView view = mAppEntitiesHeaderView.findViewById(R.id.header_title);
+
+ assertThat(view.getText()).isEqualTo(mContext.getText(R.string.expand_button_title));
+ }
+
+ @Test
+ public void setHeaderDetailsRes_setTextRes_shouldSetToDetailsView() {
+ mController.setHeaderDetailsRes(R.string.expand_button_title).apply();
+ final TextView view = mAppEntitiesHeaderView.findViewById(R.id.header_details);
+
+ assertThat(view.getText()).isEqualTo(mContext.getText(R.string.expand_button_title));
+ }
+
+ @Test
+ public void setHeaderDetailsClickListener_setClickListener_detailsViewAttachClickListener() {
+ mController.setHeaderDetailsClickListener(v -> {
+ }).apply();
+ final TextView view = mAppEntitiesHeaderView.findViewById(R.id.header_details);
+
+ assertThat(view.hasOnClickListeners()).isTrue();
+ }
+
+ @Test
+ public void setAppEntity_indexLessThanZero_shouldThrowArrayIndexOutOfBoundsException() {
+ thrown.expect(ArrayIndexOutOfBoundsException.class);
+
+ mController.setAppEntity(-1, mIcon, TITLE, SUMMARY);
+ }
+
+ @Test
+ public void asetAppEntity_indexGreaterThanMaximum_shouldThrowArrayIndexOutOfBoundsException() {
+ thrown.expect(ArrayIndexOutOfBoundsException.class);
+
+ mController.setAppEntity(AppEntitiesHeaderController.MAXIMUM_APPS + 1, mIcon, TITLE,
+ SUMMARY);
+ }
+
+ @Test
+ public void setAppEntity_addAppToIndex0_shouldShowAppView1() {
+ mController.setAppEntity(0, mIcon, TITLE, SUMMARY).apply();
+ final View app1View = mAppEntitiesHeaderView.findViewById(R.id.app1_view);
+ final ImageView appIconView = app1View.findViewById(R.id.app_icon);
+ final TextView appTitle = app1View.findViewById(R.id.app_title);
+ final TextView appSummary = app1View.findViewById(R.id.app_summary);
+
+ assertThat(app1View.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(appIconView.getDrawable()).isNotNull();
+ assertThat(appTitle.getText()).isEqualTo(TITLE);
+ assertThat(appSummary.getText()).isEqualTo(SUMMARY);
+ }
+
+ @Test
+ public void setAppEntity_addAppToIndex1_shouldShowAppView2() {
+ mController.setAppEntity(1, mIcon, TITLE, SUMMARY).apply();
+ final View app2View = mAppEntitiesHeaderView.findViewById(R.id.app2_view);
+ final ImageView appIconView = app2View.findViewById(R.id.app_icon);
+ final TextView appTitle = app2View.findViewById(R.id.app_title);
+ final TextView appSummary = app2View.findViewById(R.id.app_summary);
+
+ assertThat(app2View.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(appIconView.getDrawable()).isNotNull();
+ assertThat(appTitle.getText()).isEqualTo(TITLE);
+ assertThat(appSummary.getText()).isEqualTo(SUMMARY);
+ }
+
+ @Test
+ public void setAppEntity_addAppToIndex2_shouldShowAppView3() {
+ mController.setAppEntity(2, mIcon, TITLE, SUMMARY).apply();
+ final View app3View = mAppEntitiesHeaderView.findViewById(R.id.app3_view);
+ final ImageView appIconView = app3View.findViewById(R.id.app_icon);
+ final TextView appTitle = app3View.findViewById(R.id.app_title);
+ final TextView appSummary = app3View.findViewById(R.id.app_summary);
+
+ assertThat(app3View.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(appIconView.getDrawable()).isNotNull();
+ assertThat(appTitle.getText()).isEqualTo(TITLE);
+ assertThat(appSummary.getText()).isEqualTo(SUMMARY);
+ }
+
+ @Test
+ public void removeAppEntity_removeIndex0_shouldNotShowAppView1() {
+ mController.setAppEntity(0, mIcon, TITLE, SUMMARY)
+ .setAppEntity(1, mIcon, TITLE, SUMMARY).apply();
+ final View app1View = mAppEntitiesHeaderView.findViewById(R.id.app1_view);
+ final View app2View = mAppEntitiesHeaderView.findViewById(R.id.app2_view);
+
+ assertThat(app1View.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(app2View.getVisibility()).isEqualTo(View.VISIBLE);
+
+ mController.removeAppEntity(0).apply();
+
+ assertThat(app1View.getVisibility()).isEqualTo(View.GONE);
+ assertThat(app2View.getVisibility()).isEqualTo(View.VISIBLE);
+ }
+
+ @Test
+ public void clearAllAppEntities_shouldNotShowAllAppViews() {
+ mController.setAppEntity(0, mIcon, TITLE, SUMMARY)
+ .setAppEntity(1, mIcon, TITLE, SUMMARY)
+ .setAppEntity(2, mIcon, TITLE, SUMMARY).apply();
+ final View app1View = mAppEntitiesHeaderView.findViewById(R.id.app1_view);
+ final View app2View = mAppEntitiesHeaderView.findViewById(R.id.app2_view);
+ final View app3View = mAppEntitiesHeaderView.findViewById(R.id.app3_view);
+
+ assertThat(app1View.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(app2View.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(app3View.getVisibility()).isEqualTo(View.VISIBLE);
+
+ mController.clearAllAppEntities().apply();
+ assertThat(app1View.getVisibility()).isEqualTo(View.GONE);
+ assertThat(app2View.getVisibility()).isEqualTo(View.GONE);
+ assertThat(app3View.getVisibility()).isEqualTo(View.GONE);
+ }
+}
diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java
index 14503f9..eda9fe1 100644
--- a/services/core/java/com/android/server/ConnectivityService.java
+++ b/services/core/java/com/android/server/ConnectivityService.java
@@ -902,6 +902,7 @@
// Listen to package add and removal events for all users.
intentFilter = new IntentFilter();
intentFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
+ intentFilter.addAction(Intent.ACTION_PACKAGE_REPLACED);
intentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
intentFilter.addDataScheme("package");
mContext.registerReceiverAsUser(
@@ -4203,12 +4204,46 @@
mPermissionMonitor.onPackageAdded(packageName, uid);
}
- private void onPackageRemoved(String packageName, int uid) {
+ private void onPackageReplaced(String packageName, int uid) {
+ if (TextUtils.isEmpty(packageName) || uid < 0) {
+ Slog.wtf(TAG, "Invalid package in onPackageReplaced: " + packageName + " | " + uid);
+ return;
+ }
+ final int userId = UserHandle.getUserId(uid);
+ synchronized (mVpns) {
+ final Vpn vpn = mVpns.get(userId);
+ if (vpn == null) {
+ return;
+ }
+ // Legacy always-on VPN won't be affected since the package name is not set.
+ if (TextUtils.equals(vpn.getAlwaysOnPackage(), packageName)) {
+ Slog.d(TAG, "Restarting always-on VPN package " + packageName + " for user "
+ + userId);
+ vpn.startAlwaysOnVpn();
+ }
+ }
+ }
+
+ private void onPackageRemoved(String packageName, int uid, boolean isReplacing) {
if (TextUtils.isEmpty(packageName) || uid < 0) {
Slog.wtf(TAG, "Invalid package in onPackageRemoved: " + packageName + " | " + uid);
return;
}
mPermissionMonitor.onPackageRemoved(uid);
+
+ final int userId = UserHandle.getUserId(uid);
+ synchronized (mVpns) {
+ final Vpn vpn = mVpns.get(userId);
+ if (vpn == null) {
+ return;
+ }
+ // Legacy always-on VPN won't be affected since the package name is not set.
+ if (TextUtils.equals(vpn.getAlwaysOnPackage(), packageName) && !isReplacing) {
+ Slog.d(TAG, "Removing always-on VPN package " + packageName + " for user "
+ + userId);
+ vpn.setAlwaysOnPackage(null, false);
+ }
+ }
}
private void onUserUnlocked(int userId) {
@@ -4245,8 +4280,12 @@
onUserUnlocked(userId);
} else if (Intent.ACTION_PACKAGE_ADDED.equals(action)) {
onPackageAdded(packageName, uid);
+ } else if (Intent.ACTION_PACKAGE_REPLACED.equals(action)) {
+ onPackageReplaced(packageName, uid);
} else if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) {
- onPackageRemoved(packageName, uid);
+ final boolean isReplacing = intent.getBooleanExtra(
+ Intent.EXTRA_REPLACING, false);
+ onPackageRemoved(packageName, uid, isReplacing);
}
}
};
diff --git a/services/core/java/com/android/server/connectivity/Tethering.java b/services/core/java/com/android/server/connectivity/Tethering.java
index 3c14393..d75601b 100644
--- a/services/core/java/com/android/server/connectivity/Tethering.java
+++ b/services/core/java/com/android/server/connectivity/Tethering.java
@@ -944,10 +944,11 @@
public boolean hasTetherableConfiguration() {
final TetheringConfiguration cfg = mConfig;
final boolean hasDownstreamConfiguration =
- (cfg.tetherableUsbRegexs.length != 0) ||
- (cfg.tetherableWifiRegexs.length != 0) ||
- (cfg.tetherableBluetoothRegexs.length != 0);
- final boolean hasUpstreamConfiguration = !cfg.preferredUpstreamIfaceTypes.isEmpty();
+ (cfg.tetherableUsbRegexs.length != 0)
+ || (cfg.tetherableWifiRegexs.length != 0)
+ || (cfg.tetherableBluetoothRegexs.length != 0);
+ final boolean hasUpstreamConfiguration = !cfg.preferredUpstreamIfaceTypes.isEmpty()
+ || cfg.chooseUpstreamAutomatically;
return hasDownstreamConfiguration && hasUpstreamConfiguration;
}
diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java
index b7ed2f9..602aedb 100644
--- a/services/core/java/com/android/server/connectivity/Vpn.java
+++ b/services/core/java/com/android/server/connectivity/Vpn.java
@@ -206,45 +206,6 @@
// Handle of the user initiating VPN.
private final int mUserHandle;
- // Listen to package removal and change events (update/uninstall) for this user
- private final BroadcastReceiver mPackageIntentReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- final Uri data = intent.getData();
- final String packageName = data == null ? null : data.getSchemeSpecificPart();
- if (packageName == null) {
- return;
- }
-
- synchronized (Vpn.this) {
- // Avoid race where always-on package has been unset
- if (!packageName.equals(getAlwaysOnPackage())) {
- return;
- }
-
- final String action = intent.getAction();
- Log.i(TAG, "Received broadcast " + action + " for always-on VPN package "
- + packageName + " in user " + mUserHandle);
-
- switch(action) {
- case Intent.ACTION_PACKAGE_REPLACED:
- // Start vpn after app upgrade
- startAlwaysOnVpn();
- break;
- case Intent.ACTION_PACKAGE_REMOVED:
- final boolean isPackageRemoved = !intent.getBooleanExtra(
- Intent.EXTRA_REPLACING, false);
- if (isPackageRemoved) {
- setAlwaysOnPackage(null, false);
- }
- break;
- }
- }
- }
- };
-
- private boolean mIsPackageIntentReceiverRegistered = false;
-
public Vpn(Looper looper, Context context, INetworkManagementService netService,
@UserIdInt int userHandle) {
this(looper, context, netService, userHandle, new SystemServices(context));
@@ -500,7 +461,6 @@
// Prepare this app. The notification will update as a side-effect of updateState().
prepareInternal(packageName);
}
- maybeRegisterPackageChangeReceiverLocked(packageName);
setVpnForcedLocked(mLockdown);
return true;
}
@@ -509,31 +469,6 @@
return packageName == null || VpnConfig.LEGACY_VPN.equals(packageName);
}
- private void unregisterPackageChangeReceiverLocked() {
- if (mIsPackageIntentReceiverRegistered) {
- mContext.unregisterReceiver(mPackageIntentReceiver);
- mIsPackageIntentReceiverRegistered = false;
- }
- }
-
- private void maybeRegisterPackageChangeReceiverLocked(String packageName) {
- // Unregister IntentFilter listening for previous always-on package change
- unregisterPackageChangeReceiverLocked();
-
- if (!isNullOrLegacyVpn(packageName)) {
- mIsPackageIntentReceiverRegistered = true;
-
- IntentFilter intentFilter = new IntentFilter();
- // Protected intent can only be sent by system. No permission required in register.
- intentFilter.addAction(Intent.ACTION_PACKAGE_REPLACED);
- intentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
- intentFilter.addDataScheme("package");
- intentFilter.addDataSchemeSpecificPart(packageName, PatternMatcher.PATTERN_LITERAL);
- mContext.registerReceiverAsUser(
- mPackageIntentReceiver, UserHandle.of(mUserHandle), intentFilter, null, null);
- }
- }
-
/**
* @return the package name of the VPN controller responsible for always-on VPN,
* or {@code null} if none is set or always-on VPN is controlled through
@@ -1302,7 +1237,6 @@
setLockdown(false);
mAlwaysOn = false;
- unregisterPackageChangeReceiverLocked();
// Quit any active connections
agentDisconnect();
}
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index d96b6cb..e7c3c7b 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -1951,6 +1951,11 @@
}
// Native callback.
+ private int getPointerDisplayId() {
+ return mWindowManagerCallbacks.getPointerDisplayId();
+ }
+
+ // Native callback.
private String[] getKeyboardLayoutOverlay(InputDeviceIdentifier identifier) {
if (!mSystemReady) {
return null;
@@ -2017,6 +2022,8 @@
KeyEvent event, int policyFlags);
public int getPointerLayer();
+
+ public int getPointerDisplayId();
}
/**
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/certificate/CertUtils.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/certificate/CertUtils.java
index 6e08949..26e8270 100644
--- a/services/core/java/com/android/server/locksettings/recoverablekeystore/certificate/CertUtils.java
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/certificate/CertUtils.java
@@ -16,13 +16,17 @@
package com.android.server.locksettings.recoverablekeystore.certificate;
-import static javax.xml.xpath.XPathConstants.NODESET;
-
import android.annotation.IntDef;
import android.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.SAXException;
+
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
@@ -40,7 +44,6 @@
import java.security.cert.CertPathValidator;
import java.security.cert.CertPathValidatorException;
import java.security.cert.CertStore;
-import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.CollectionCertStoreParameters;
@@ -58,15 +61,6 @@
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
-import javax.xml.xpath.XPath;
-import javax.xml.xpath.XPathExpressionException;
-import javax.xml.xpath.XPathFactory;
-
-import org.w3c.dom.Document;
-import org.w3c.dom.Element;
-import org.w3c.dom.Node;
-import org.w3c.dom.NodeList;
-import org.xml.sax.SAXException;
/** Utility functions related to parsing and validating public-key certificates. */
public final class CertUtils {
@@ -167,50 +161,63 @@
static List<String> getXmlNodeContents(@MustExist int mustExist, Element rootNode,
String... nodeTags)
throws CertParsingException {
- String expression = String.join("/", nodeTags);
-
- XPath xPath = XPathFactory.newInstance().newXPath();
- NodeList nodeList;
- try {
- nodeList = (NodeList) xPath.compile(expression).evaluate(rootNode, NODESET);
- } catch (XPathExpressionException e) {
- throw new CertParsingException(e);
+ if (nodeTags.length == 0) {
+ throw new CertParsingException("The tag list must not be empty");
}
- switch (mustExist) {
- case MUST_EXIST_UNENFORCED:
- break;
-
- case MUST_EXIST_EXACTLY_ONE:
- if (nodeList.getLength() != 1) {
- throw new CertParsingException(
- "The XML file must contain exactly one node with the path "
- + expression);
- }
- break;
-
- case MUST_EXIST_AT_LEAST_ONE:
- if (nodeList.getLength() == 0) {
- throw new CertParsingException(
- "The XML file must contain at least one node with the path "
- + expression);
- }
- break;
-
- default:
- throw new UnsupportedOperationException(
- "This value of MustExist is not supported: " + mustExist);
+ // Go down through all the intermediate node tags (except the last tag for the leaf nodes).
+ // Note that this implementation requires that at most one path exists for the given
+ // intermediate node tags.
+ Element parent = rootNode;
+ for (int i = 0; i < nodeTags.length - 1; i++) {
+ String tag = nodeTags[i];
+ List<Element> children = getXmlDirectChildren(parent, tag);
+ if ((children.size() == 0 && mustExist != MUST_EXIST_UNENFORCED)
+ || children.size() > 1) {
+ throw new CertParsingException(
+ "The XML file must contain exactly one path with the tag " + tag);
+ }
+ if (children.size() == 0) {
+ return new ArrayList<>();
+ }
+ parent = children.get(0);
}
+ // Then collect the contents of the leaf nodes.
+ List<Element> leafs = getXmlDirectChildren(parent, nodeTags[nodeTags.length - 1]);
+ if (mustExist == MUST_EXIST_EXACTLY_ONE && leafs.size() != 1) {
+ throw new CertParsingException(
+ "The XML file must contain exactly one node with the path "
+ + String.join("/", nodeTags));
+ }
+ if (mustExist == MUST_EXIST_AT_LEAST_ONE && leafs.size() == 0) {
+ throw new CertParsingException(
+ "The XML file must contain at least one node with the path "
+ + String.join("/", nodeTags));
+ }
List<String> result = new ArrayList<>();
- for (int i = 0; i < nodeList.getLength(); i++) {
- Node node = nodeList.item(i);
+ for (Element leaf : leafs) {
// Remove whitespaces and newlines.
- result.add(node.getTextContent().replaceAll("\\s", ""));
+ result.add(leaf.getTextContent().replaceAll("\\s", ""));
}
return result;
}
+ /** Get the direct child nodes with a given tag. */
+ private static List<Element> getXmlDirectChildren(Element parent, String tag) {
+ // Cannot use Element.getElementsByTagName because it will return all descendant elements
+ // with the tag name, i.e. not only the direct child nodes.
+ List<Element> children = new ArrayList<>();
+ NodeList childNodes = parent.getChildNodes();
+ for (int i = 0; i < childNodes.getLength(); i++) {
+ Node node = childNodes.item(i);
+ if (node.getNodeType() == Node.ELEMENT_NODE && node.getNodeName().equals(tag)) {
+ children.add((Element) node);
+ }
+ }
+ return children;
+ }
+
/**
* Decodes a base64-encoded string.
*
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 888675c..b36ac98 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -86,11 +86,6 @@
import static android.content.pm.PackageManager.PERMISSION_DENIED;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.content.pm.PackageParser.isApkFile;
-import static android.content.pm.SharedLibraryNames.ANDROID_HIDL_BASE;
-import static android.content.pm.SharedLibraryNames.ANDROID_HIDL_MANAGER;
-import static android.content.pm.SharedLibraryNames.ANDROID_TEST_BASE;
-import static android.content.pm.SharedLibraryNames.ANDROID_TEST_MOCK;
-import static android.content.pm.SharedLibraryNames.ANDROID_TEST_RUNNER;
import static android.os.Trace.TRACE_TAG_PACKAGE_MANAGER;
import static android.os.storage.StorageManager.FLAG_STORAGE_CE;
import static android.os.storage.StorageManager.FLAG_STORAGE_DE;
@@ -2094,28 +2089,6 @@
}
}
- @GuardedBy("mPackages")
- private void setupBuiltinSharedLibraryDependenciesLocked() {
- // Builtin libraries don't have versions.
- long version = SharedLibraryInfo.VERSION_UNDEFINED;
-
- SharedLibraryInfo libraryInfo = getSharedLibraryInfoLPr(ANDROID_HIDL_MANAGER, version);
- if (libraryInfo != null) {
- libraryInfo.addDependency(getSharedLibraryInfoLPr(ANDROID_HIDL_BASE, version));
- }
-
- libraryInfo = getSharedLibraryInfoLPr(ANDROID_TEST_RUNNER, version);
- if (libraryInfo != null) {
- libraryInfo.addDependency(getSharedLibraryInfoLPr(ANDROID_TEST_MOCK, version));
- libraryInfo.addDependency(getSharedLibraryInfoLPr(ANDROID_TEST_BASE, version));
- }
-
- libraryInfo = getSharedLibraryInfoLPr(ANDROID_TEST_MOCK, version);
- if (libraryInfo != null) {
- libraryInfo.addDependency(getSharedLibraryInfoLPr(ANDROID_TEST_BASE, version));
- }
- }
-
public PackageManagerService(Context context, Installer installer,
boolean factoryTest, boolean onlyCore) {
LockGuard.installLock(mPackages, LockGuard.INDEX_PACKAGES);
@@ -2223,17 +2196,32 @@
Watchdog.getInstance().addThread(mHandler, WATCHDOG_TIMEOUT);
mInstantAppRegistry = new InstantAppRegistry(this);
- ArrayMap<String, String> libConfig = systemConfig.getSharedLibraries();
+ ArrayMap<String, SystemConfig.SharedLibraryEntry> libConfig
+ = systemConfig.getSharedLibraries();
final int builtInLibCount = libConfig.size();
for (int i = 0; i < builtInLibCount; i++) {
String name = libConfig.keyAt(i);
- String path = libConfig.valueAt(i);
- addSharedLibraryLPw(path, null, null, name, SharedLibraryInfo.VERSION_UNDEFINED,
- SharedLibraryInfo.TYPE_BUILTIN, PLATFORM_PACKAGE_NAME, 0);
+ SystemConfig.SharedLibraryEntry entry = libConfig.valueAt(i);
+ addSharedLibraryLPw(entry.filename, null, null, name,
+ SharedLibraryInfo.VERSION_UNDEFINED, SharedLibraryInfo.TYPE_BUILTIN,
+ PLATFORM_PACKAGE_NAME, 0);
}
- // Builtin libraries cannot encode their dependency where they are
- // defined, so fix that now.
- setupBuiltinSharedLibraryDependenciesLocked();
+
+ // Now that we have added all the libraries, iterate again to add dependency
+ // information IFF their dependencies are added.
+ long undefinedVersion = SharedLibraryInfo.VERSION_UNDEFINED;
+ for (int i = 0; i < builtInLibCount; i++) {
+ String name = libConfig.keyAt(i);
+ SystemConfig.SharedLibraryEntry entry = libConfig.valueAt(i);
+ final int dependencyCount = entry.dependencies.length;
+ for (int j = 0; j < dependencyCount; j++) {
+ final SharedLibraryInfo dependency =
+ getSharedLibraryInfoLPr(entry.dependencies[j], undefinedVersion);
+ if (dependency != null) {
+ getSharedLibraryInfoLPr(name, undefinedVersion).addDependency(dependency);
+ }
+ }
+ }
SELinuxMMAC.readInstallPolicy();
diff --git a/services/core/java/com/android/server/pm/dex/DexManager.java b/services/core/java/com/android/server/pm/dex/DexManager.java
index 3a74ab5..36b7269 100644
--- a/services/core/java/com/android/server/pm/dex/DexManager.java
+++ b/services/core/java/com/android/server/pm/dex/DexManager.java
@@ -16,6 +16,11 @@
package com.android.server.pm.dex;
+import static com.android.server.pm.InstructionSets.getAppDexInstructionSets;
+import static com.android.server.pm.dex.PackageDexUsage.DexUseInfo;
+import static com.android.server.pm.dex.PackageDexUsage.PackageUseInfo;
+import static com.android.server.pm.dex.PackageDynamicCodeLoading.PackageDynamicCode;
+
import android.content.ContentResolver;
import android.content.Context;
import android.content.pm.ApplicationInfo;
@@ -26,9 +31,9 @@
import android.os.Build;
import android.os.FileUtils;
import android.os.RemoteException;
-import android.os.storage.StorageManager;
import android.os.SystemProperties;
import android.os.UserHandle;
+import android.os.storage.StorageManager;
import android.provider.Settings.Global;
import android.util.Log;
import android.util.Slog;
@@ -48,18 +53,14 @@
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
-import java.util.Iterator;
-import java.util.List;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.zip.ZipEntry;
-import static com.android.server.pm.InstructionSets.getAppDexInstructionSets;
-import static com.android.server.pm.dex.PackageDexUsage.PackageUseInfo;
-import static com.android.server.pm.dex.PackageDexUsage.DexUseInfo;
-
/**
* This class keeps track of how dex files are used.
* Every time it gets a notification about a dex file being loaded it tracks
@@ -89,6 +90,12 @@
// encode and save the dex usage data.
private final PackageDexUsage mPackageDexUsage;
+ // PackageDynamicCodeLoading handles recording of dynamic code loading -
+ // which is similar to PackageDexUsage but records a different aspect of the data.
+ // (It additionally includes DEX files loaded with unsupported class loaders, and doesn't
+ // record class loaders or ISAs.)
+ private final PackageDynamicCodeLoading mPackageDynamicCodeLoading;
+
private final IPackageManager mPackageManager;
private final PackageDexOptimizer mPackageDexOptimizer;
private final Object mInstallLock;
@@ -126,14 +133,15 @@
public DexManager(Context context, IPackageManager pms, PackageDexOptimizer pdo,
Installer installer, Object installLock, Listener listener) {
- mContext = context;
- mPackageCodeLocationsCache = new HashMap<>();
- mPackageDexUsage = new PackageDexUsage();
- mPackageManager = pms;
- mPackageDexOptimizer = pdo;
- mInstaller = installer;
- mInstallLock = installLock;
- mListener = listener;
+ mContext = context;
+ mPackageCodeLocationsCache = new HashMap<>();
+ mPackageDexUsage = new PackageDexUsage();
+ mPackageDynamicCodeLoading = new PackageDynamicCodeLoading();
+ mPackageManager = pms;
+ mPackageDexOptimizer = pdo;
+ mInstaller = installer;
+ mInstallLock = installLock;
+ mListener = listener;
}
public void systemReady() {
@@ -207,7 +215,6 @@
Slog.i(TAG, loadingAppInfo.packageName +
" uses unsupported class loader in " + classLoaderNames);
}
- return;
}
int dexPathIndex = 0;
@@ -236,15 +243,24 @@
continue;
}
- // Record dex file usage. If the current usage is a new pattern (e.g. new secondary,
- // or UsedByOtherApps), record will return true and we trigger an async write
- // to disk to make sure we don't loose the data in case of a reboot.
+ if (mPackageDynamicCodeLoading.record(searchResult.mOwningPackageName, dexPath,
+ PackageDynamicCodeLoading.FILE_TYPE_DEX, loaderUserId,
+ loadingAppInfo.packageName)) {
+ mPackageDynamicCodeLoading.maybeWriteAsync();
+ }
- String classLoaderContext = classLoaderContexts[dexPathIndex];
- if (mPackageDexUsage.record(searchResult.mOwningPackageName,
- dexPath, loaderUserId, loaderIsa, isUsedByOtherApps, primaryOrSplit,
- loadingAppInfo.packageName, classLoaderContext)) {
- mPackageDexUsage.maybeWriteAsync();
+ if (classLoaderContexts != null) {
+
+ // Record dex file usage. If the current usage is a new pattern (e.g. new
+ // secondary, or UsedByOtherApps), record will return true and we trigger an
+ // async write to disk to make sure we don't loose the data in case of a reboot.
+
+ String classLoaderContext = classLoaderContexts[dexPathIndex];
+ if (mPackageDexUsage.record(searchResult.mOwningPackageName,
+ dexPath, loaderUserId, loaderIsa, isUsedByOtherApps, primaryOrSplit,
+ loadingAppInfo.packageName, classLoaderContext)) {
+ mPackageDexUsage.maybeWriteAsync();
+ }
}
} else {
// If we can't find the owner of the dex we simply do not track it. The impact is
@@ -268,8 +284,8 @@
loadInternal(existingPackages);
} catch (Exception e) {
mPackageDexUsage.clear();
- Slog.w(TAG, "Exception while loading package dex usage. " +
- "Starting with a fresh state.", e);
+ mPackageDynamicCodeLoading.clear();
+ Slog.w(TAG, "Exception while loading. Starting with a fresh state.", e);
}
}
@@ -311,15 +327,24 @@
* all usage information for the package will be removed.
*/
public void notifyPackageDataDestroyed(String packageName, int userId) {
- boolean updated = userId == UserHandle.USER_ALL
- ? mPackageDexUsage.removePackage(packageName)
- : mPackageDexUsage.removeUserPackage(packageName, userId);
// In case there was an update, write the package use info to disk async.
- // Note that we do the writing here and not in PackageDexUsage in order to be
+ // Note that we do the writing here and not in the lower level classes in order to be
// consistent with other methods in DexManager (e.g. reconcileSecondaryDexFiles performs
// multiple updates in PackageDexUsage before writing it).
- if (updated) {
- mPackageDexUsage.maybeWriteAsync();
+ if (userId == UserHandle.USER_ALL) {
+ if (mPackageDexUsage.removePackage(packageName)) {
+ mPackageDexUsage.maybeWriteAsync();
+ }
+ if (mPackageDynamicCodeLoading.removePackage(packageName)) {
+ mPackageDynamicCodeLoading.maybeWriteAsync();
+ }
+ } else {
+ if (mPackageDexUsage.removeUserPackage(packageName, userId)) {
+ mPackageDexUsage.maybeWriteAsync();
+ }
+ if (mPackageDynamicCodeLoading.removeUserPackage(packageName, userId)) {
+ mPackageDynamicCodeLoading.maybeWriteAsync();
+ }
}
}
@@ -388,8 +413,23 @@
}
}
- mPackageDexUsage.read();
- mPackageDexUsage.syncData(packageToUsersMap, packageToCodePaths);
+ try {
+ mPackageDexUsage.read();
+ mPackageDexUsage.syncData(packageToUsersMap, packageToCodePaths);
+ } catch (Exception e) {
+ mPackageDexUsage.clear();
+ Slog.w(TAG, "Exception while loading package dex usage. "
+ + "Starting with a fresh state.", e);
+ }
+
+ try {
+ mPackageDynamicCodeLoading.read();
+ mPackageDynamicCodeLoading.syncData(packageToUsersMap);
+ } catch (Exception e) {
+ mPackageDynamicCodeLoading.clear();
+ Slog.w(TAG, "Exception while loading package dynamic code usage. "
+ + "Starting with a fresh state.", e);
+ }
}
/**
@@ -415,10 +455,16 @@
* TODO(calin): maybe we should not (prune) so we can have an accurate view when we try
* to access the package use.
*/
+ @VisibleForTesting
/*package*/ boolean hasInfoOnPackage(String packageName) {
return mPackageDexUsage.getPackageUseInfo(packageName) != null;
}
+ @VisibleForTesting
+ /*package*/ PackageDynamicCode getPackageDynamicCodeInfo(String packageName) {
+ return mPackageDynamicCodeLoading.getPackageDynamicCodeInfo(packageName);
+ }
+
/**
* Perform dexopt on with the given {@code options} on the secondary dex files.
* @return true if all secondary dex files were processed successfully (compiled or skipped
@@ -652,7 +698,7 @@
// to load dex files through it.
try {
String dexPathReal = PackageManagerServiceUtils.realpath(new File(dexPath));
- if (dexPathReal != dexPath) {
+ if (!dexPath.equals(dexPathReal)) {
Slog.d(TAG, "Dex loaded with symlink. dexPath=" +
dexPath + " dexPathReal=" + dexPathReal);
}
@@ -675,6 +721,7 @@
*/
public void writePackageDexUsageNow() {
mPackageDexUsage.writeNow();
+ mPackageDynamicCodeLoading.writeNow();
}
private void registerSettingObserver() {
diff --git a/services/core/java/com/android/server/pm/dex/PackageDynamicCodeLoading.java b/services/core/java/com/android/server/pm/dex/PackageDynamicCodeLoading.java
new file mode 100644
index 0000000..f74aa1d
--- /dev/null
+++ b/services/core/java/com/android/server/pm/dex/PackageDynamicCodeLoading.java
@@ -0,0 +1,612 @@
+/*
+ * Copyright 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.pm.dex;
+
+import android.util.AtomicFile;
+import android.util.Slog;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.FastPrintWriter;
+import com.android.server.pm.AbstractStatsBase;
+
+import libcore.io.IoUtils;
+
+import java.io.BufferedReader;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Stats file which stores information about secondary code files that are dynamically loaded.
+ */
+class PackageDynamicCodeLoading extends AbstractStatsBase<Void> {
+ // Type code to indicate a secondary file containing DEX code. (The char value is how it
+ // is represented in the text file format.)
+ static final int FILE_TYPE_DEX = 'D';
+
+ private static final String TAG = "PackageDynamicCodeLoading";
+
+ private static final String FILE_VERSION_HEADER = "DCL1";
+ private static final String PACKAGE_PREFIX = "P:";
+
+ private static final char FIELD_SEPARATOR = ':';
+ private static final String PACKAGE_SEPARATOR = ",";
+
+ /**
+ * Regular expression to match the expected format of an input line describing one file.
+ * <p>Example: {@code D:10:package.name1,package.name2:/escaped/path}
+ * <p>The capturing groups are the file type, user ID, loading packages and escaped file path
+ * (in that order).
+ * <p>See {@link #write(OutputStream, Map)} below for more details of the format.
+ */
+ private static final Pattern PACKAGE_LINE_PATTERN =
+ Pattern.compile("([A-Z]):([0-9]+):([^:]*):(.*)");
+
+ private final Object mLock = new Object();
+
+ // Map from package name to data about loading of dynamic code files owned by that package.
+ // (Apps may load code files owned by other packages, subject to various access
+ // constraints.)
+ // Any PackageDynamicCode in this map will be non-empty.
+ @GuardedBy("mLock")
+ private Map<String, PackageDynamicCode> mPackageMap = new HashMap<>();
+
+ PackageDynamicCodeLoading() {
+ super("package-dcl.list", "PackageDynamicCodeLoading_DiskWriter", false);
+ }
+
+ /**
+ * Record dynamic code loading from a file.
+ *
+ * Note this is called when an app loads dex files and as such it should return
+ * as fast as possible.
+ *
+ * @param owningPackageName the package owning the file path
+ * @param filePath the path of the dex files being loaded
+ * @param fileType the type of code loading
+ * @param ownerUserId the user id which runs the code loading the file
+ * @param loadingPackageName the package performing the load
+ * @return whether new information has been recorded
+ * @throws IllegalArgumentException if clearly invalid information is detected
+ */
+ boolean record(String owningPackageName, String filePath, int fileType, int ownerUserId,
+ String loadingPackageName) {
+ if (fileType != FILE_TYPE_DEX) {
+ throw new IllegalArgumentException("Bad file type: " + fileType);
+ }
+ synchronized (mLock) {
+ PackageDynamicCode packageInfo = mPackageMap.get(owningPackageName);
+ if (packageInfo == null) {
+ packageInfo = new PackageDynamicCode();
+ mPackageMap.put(owningPackageName, packageInfo);
+ }
+ return packageInfo.add(filePath, (char) fileType, ownerUserId, loadingPackageName);
+ }
+ }
+
+ /**
+ * Return all packages that contain records of secondary dex files. (Note that data updates
+ * asynchronously, so {@link #getPackageDynamicCodeInfo} may still return null if passed
+ * one of these package names.)
+ */
+ Set<String> getAllPackagesWithDynamicCodeLoading() {
+ synchronized (mLock) {
+ return new HashSet<>(mPackageMap.keySet());
+ }
+ }
+
+ /**
+ * Return information about the dynamic code file usage of the specified package,
+ * or null if there is currently no usage information. The object returned is a copy of the
+ * live information that is not updated.
+ */
+ PackageDynamicCode getPackageDynamicCodeInfo(String packageName) {
+ synchronized (mLock) {
+ PackageDynamicCode info = mPackageMap.get(packageName);
+ return info == null ? null : new PackageDynamicCode(info);
+ }
+ }
+
+ /**
+ * Remove all information about all packages.
+ */
+ void clear() {
+ synchronized (mLock) {
+ mPackageMap.clear();
+ }
+ }
+
+ /**
+ * Remove the data associated with package {@code packageName}. Affects all users.
+ * @return true if the package usage was found and removed successfully
+ */
+ boolean removePackage(String packageName) {
+ synchronized (mLock) {
+ return mPackageMap.remove(packageName) != null;
+ }
+ }
+
+ /**
+ * Remove all the records about package {@code packageName} belonging to user {@code userId}.
+ * @return whether any data was actually removed
+ */
+ boolean removeUserPackage(String packageName, int userId) {
+ synchronized (mLock) {
+ PackageDynamicCode packageDynamicCode = mPackageMap.get(packageName);
+ if (packageDynamicCode == null) {
+ return false;
+ }
+ if (packageDynamicCode.removeUser(userId)) {
+ if (packageDynamicCode.mFileUsageMap.isEmpty()) {
+ mPackageMap.remove(packageName);
+ }
+ return true;
+ } else {
+ return false;
+ }
+ }
+ }
+
+ /**
+ * Remove the specified dynamic code file record belonging to the package {@code packageName}
+ * and user {@code userId}.
+ * @return whether data was actually removed
+ */
+ boolean removeFile(String packageName, String filePath, int userId) {
+ synchronized (mLock) {
+ PackageDynamicCode packageDynamicCode = mPackageMap.get(packageName);
+ if (packageDynamicCode == null) {
+ return false;
+ }
+ if (packageDynamicCode.removeFile(filePath, userId)) {
+ if (packageDynamicCode.mFileUsageMap.isEmpty()) {
+ mPackageMap.remove(packageName);
+ }
+ return true;
+ } else {
+ return false;
+ }
+ }
+ }
+
+ /**
+ * Syncs data with the set of installed packages. Data about packages that are no longer
+ * installed is removed.
+ * @param packageToUsersMap a map from all existing package names to the users who have the
+ * package installed
+ */
+ void syncData(Map<String, Set<Integer>> packageToUsersMap) {
+ synchronized (mLock) {
+ Iterator<Entry<String, PackageDynamicCode>> it = mPackageMap.entrySet().iterator();
+ while (it.hasNext()) {
+ Entry<String, PackageDynamicCode> entry = it.next();
+ Set<Integer> packageUsers = packageToUsersMap.get(entry.getKey());
+ if (packageUsers == null) {
+ it.remove();
+ } else {
+ PackageDynamicCode packageDynamicCode = entry.getValue();
+ packageDynamicCode.syncData(packageToUsersMap, packageUsers);
+ if (packageDynamicCode.mFileUsageMap.isEmpty()) {
+ it.remove();
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Request that data be written to persistent file at the next time allowed by write-limiting.
+ */
+ void maybeWriteAsync() {
+ super.maybeWriteAsync(null);
+ }
+
+ /**
+ * Writes data to persistent file immediately.
+ */
+ void writeNow() {
+ super.writeNow(null);
+ }
+
+ @Override
+ protected final void writeInternal(Void data) {
+ AtomicFile file = getFile();
+ FileOutputStream output = null;
+ try {
+ output = file.startWrite();
+ write(output);
+ file.finishWrite(output);
+ } catch (IOException e) {
+ file.failWrite(output);
+ Slog.e(TAG, "Failed to write dynamic usage for secondary code files.", e);
+ }
+ }
+
+ @VisibleForTesting
+ void write(OutputStream output) throws IOException {
+ // Make a deep copy to avoid holding the lock while writing to disk.
+ Map<String, PackageDynamicCode> copiedMap;
+ synchronized (mLock) {
+ copiedMap = new HashMap<>(mPackageMap.size());
+ for (Entry<String, PackageDynamicCode> entry : mPackageMap.entrySet()) {
+ PackageDynamicCode copiedValue = new PackageDynamicCode(entry.getValue());
+ copiedMap.put(entry.getKey(), copiedValue);
+ }
+ }
+
+ write(output, copiedMap);
+ }
+
+ /**
+ * Write the dynamic code loading data as a text file to {@code output}. The file format begins
+ * with a line indicating the file type and version - {@link #FILE_VERSION_HEADER}.
+ * <p>There is then one section for each owning package, introduced by a line beginning "P:".
+ * This is followed by a line for each file owned by the package this is dynamically loaded,
+ * containing the file type, user ID, loading package names and full path (with newlines and
+ * backslashes escaped - see {@link #escape}).
+ * <p>For example:
+ * <pre>{@code
+ * DCL1
+ * P:first.owning.package
+ * D:0:loading.package_1,loading.package_2:/path/to/file
+ * D:10:loading.package_1:/another/file
+ * P:second.owning.package
+ * D:0:loading.package:/third/file
+ * }</pre>
+ */
+ private static void write(OutputStream output, Map<String, PackageDynamicCode> packageMap)
+ throws IOException {
+ PrintWriter writer = new FastPrintWriter(output);
+
+ writer.println(FILE_VERSION_HEADER);
+ for (Entry<String, PackageDynamicCode> packageEntry : packageMap.entrySet()) {
+ writer.print(PACKAGE_PREFIX);
+ writer.println(packageEntry.getKey());
+
+ Map<String, DynamicCodeFile> mFileUsageMap = packageEntry.getValue().mFileUsageMap;
+ for (Entry<String, DynamicCodeFile> fileEntry : mFileUsageMap.entrySet()) {
+ String path = fileEntry.getKey();
+ DynamicCodeFile dynamicCodeFile = fileEntry.getValue();
+
+ writer.print(dynamicCodeFile.mFileType);
+ writer.print(FIELD_SEPARATOR);
+ writer.print(dynamicCodeFile.mUserId);
+ writer.print(FIELD_SEPARATOR);
+
+ String prefix = "";
+ for (String packageName : dynamicCodeFile.mLoadingPackages) {
+ writer.print(prefix);
+ writer.print(packageName);
+ prefix = PACKAGE_SEPARATOR;
+ }
+
+ writer.print(FIELD_SEPARATOR);
+ writer.println(escape(path));
+ }
+ }
+
+ writer.flush();
+ if (writer.checkError()) {
+ throw new IOException("Writer failed");
+ }
+ }
+
+ /**
+ * Read data from the persistent file. Replaces existing data completely if successful.
+ */
+ void read() {
+ super.read(null);
+ }
+
+ @Override
+ protected final void readInternal(Void data) {
+ AtomicFile file = getFile();
+
+ FileInputStream stream = null;
+ try {
+ stream = file.openRead();
+ read(stream);
+ } catch (FileNotFoundException expected) {
+ // The file may not be there. E.g. When we first take the OTA with this feature.
+ } catch (IOException e) {
+ Slog.w(TAG, "Failed to parse dynamic usage for secondary code files.", e);
+ } finally {
+ IoUtils.closeQuietly(stream);
+ }
+ }
+
+ @VisibleForTesting
+ void read(InputStream stream) throws IOException {
+ Map<String, PackageDynamicCode> newPackageMap = new HashMap<>();
+ read(stream, newPackageMap);
+ synchronized (mLock) {
+ mPackageMap = newPackageMap;
+ }
+ }
+
+ private static void read(InputStream stream, Map<String, PackageDynamicCode> packageMap)
+ throws IOException {
+ BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
+
+ String versionLine = reader.readLine();
+ if (!FILE_VERSION_HEADER.equals(versionLine)) {
+ throw new IOException("Incorrect version line: " + versionLine);
+ }
+
+ String line = reader.readLine();
+ if (line != null && !line.startsWith(PACKAGE_PREFIX)) {
+ throw new IOException("Malformed line: " + line);
+ }
+
+ while (line != null) {
+ String packageName = line.substring(PACKAGE_PREFIX.length());
+
+ PackageDynamicCode packageInfo = new PackageDynamicCode();
+ while (true) {
+ line = reader.readLine();
+ if (line == null || line.startsWith(PACKAGE_PREFIX)) {
+ break;
+ }
+ readFileInfo(line, packageInfo);
+ }
+
+ if (!packageInfo.mFileUsageMap.isEmpty()) {
+ packageMap.put(packageName, packageInfo);
+ }
+ }
+ }
+
+ private static void readFileInfo(String line, PackageDynamicCode output) throws IOException {
+ try {
+ Matcher matcher = PACKAGE_LINE_PATTERN.matcher(line);
+ if (!matcher.matches()) {
+ throw new IOException("Malformed line: " + line);
+ }
+
+ char type = matcher.group(1).charAt(0);
+ int user = Integer.parseInt(matcher.group(2));
+ String[] packages = matcher.group(3).split(PACKAGE_SEPARATOR);
+ String path = unescape(matcher.group(4));
+
+ if (packages.length == 0) {
+ throw new IOException("Malformed line: " + line);
+ }
+ if (type != FILE_TYPE_DEX) {
+ throw new IOException("Unknown file type: " + line);
+ }
+
+ output.mFileUsageMap.put(path, new DynamicCodeFile(type, user, packages));
+ } catch (RuntimeException e) {
+ // Just in case we get NumberFormatException, or various
+ // impossible out of bounds errors happen.
+ throw new IOException("Unable to parse line: " + line, e);
+ }
+ }
+
+ /**
+ * Escape any newline and backslash characters in path. A newline in a path is legal if unusual,
+ * and it would break our line-based file parsing.
+ */
+ @VisibleForTesting
+ static String escape(String path) {
+ if (path.indexOf('\\') == -1 && path.indexOf('\n') == -1 && path.indexOf('\r') == -1) {
+ return path;
+ }
+
+ StringBuilder result = new StringBuilder(path.length() + 10);
+ for (int i = 0; i < path.length(); i++) {
+ // Surrogates will never match the characters we care about, so it's ok to use chars
+ // not code points here.
+ char c = path.charAt(i);
+ switch (c) {
+ case '\\':
+ result.append("\\\\");
+ break;
+ case '\n':
+ result.append("\\n");
+ break;
+ case '\r':
+ result.append("\\r");
+ break;
+ default:
+ result.append(c);
+ break;
+ }
+ }
+ return result.toString();
+ }
+
+ /**
+ * Reverse the effect of {@link #escape}.
+ * @throws IOException if the input string is malformed
+ */
+ @VisibleForTesting
+ static String unescape(String escaped) throws IOException {
+ // As we move through the input string, start is the position of the first character
+ // after the previous escape sequence and finish is the position of the following backslash.
+ int start = 0;
+ int finish = escaped.indexOf('\\');
+ if (finish == -1) {
+ return escaped;
+ }
+
+ StringBuilder result = new StringBuilder(escaped.length());
+ while (true) {
+ if (finish >= escaped.length() - 1) {
+ // Backslash mustn't be the last character
+ throw new IOException("Unexpected \\ in: " + escaped);
+ }
+ result.append(escaped, start, finish);
+ switch (escaped.charAt(finish + 1)) {
+ case '\\':
+ result.append('\\');
+ break;
+ case 'r':
+ result.append('\r');
+ break;
+ case 'n':
+ result.append('\n');
+ break;
+ default:
+ throw new IOException("Bad escape in: " + escaped);
+ }
+
+ start = finish + 2;
+ finish = escaped.indexOf('\\', start);
+ if (finish == -1) {
+ result.append(escaped, start, escaped.length());
+ break;
+ }
+ }
+ return result.toString();
+ }
+
+ /**
+ * Represents the dynamic code usage of a single package.
+ */
+ static class PackageDynamicCode {
+ /**
+ * Map from secondary code file path to information about which packages dynamically load
+ * that file.
+ */
+ final Map<String, DynamicCodeFile> mFileUsageMap;
+
+ private PackageDynamicCode() {
+ mFileUsageMap = new HashMap<>();
+ }
+
+ private PackageDynamicCode(PackageDynamicCode original) {
+ mFileUsageMap = new HashMap<>(original.mFileUsageMap.size());
+ for (Entry<String, DynamicCodeFile> entry : original.mFileUsageMap.entrySet()) {
+ DynamicCodeFile newValue = new DynamicCodeFile(entry.getValue());
+ mFileUsageMap.put(entry.getKey(), newValue);
+ }
+ }
+
+ private boolean add(String path, char fileType, int userId, String loadingPackage) {
+ DynamicCodeFile fileInfo = mFileUsageMap.get(path);
+ if (fileInfo == null) {
+ fileInfo = new DynamicCodeFile(fileType, userId, loadingPackage);
+ mFileUsageMap.put(path, fileInfo);
+ return true;
+ } else {
+ if (fileInfo.mUserId != userId) {
+ // This should be impossible: private app files are always user-specific and
+ // can't be accessed from different users.
+ throw new IllegalArgumentException("Cannot change userId for '" + path
+ + "' from " + fileInfo.mUserId + " to " + userId);
+ }
+ // Changing file type (i.e. loading the same file in different ways is possible if
+ // unlikely. We allow it but ignore it.
+ return fileInfo.mLoadingPackages.add(loadingPackage);
+ }
+ }
+
+ private boolean removeUser(int userId) {
+ boolean updated = false;
+ Iterator<DynamicCodeFile> it = mFileUsageMap.values().iterator();
+ while (it.hasNext()) {
+ DynamicCodeFile fileInfo = it.next();
+ if (fileInfo.mUserId == userId) {
+ it.remove();
+ updated = true;
+ }
+ }
+ return updated;
+ }
+
+ private boolean removeFile(String filePath, int userId) {
+ DynamicCodeFile fileInfo = mFileUsageMap.get(filePath);
+ if (fileInfo == null || fileInfo.mUserId != userId) {
+ return false;
+ } else {
+ mFileUsageMap.remove(filePath);
+ return true;
+ }
+ }
+
+ private void syncData(Map<String, Set<Integer>> packageToUsersMap,
+ Set<Integer> owningPackageUsers) {
+ Iterator<DynamicCodeFile> fileIt = mFileUsageMap.values().iterator();
+ while (fileIt.hasNext()) {
+ DynamicCodeFile fileInfo = fileIt.next();
+ int fileUserId = fileInfo.mUserId;
+ if (!owningPackageUsers.contains(fileUserId)) {
+ fileIt.remove();
+ } else {
+ // Also remove information about any loading packages that are no longer
+ // installed for this user.
+ Iterator<String> loaderIt = fileInfo.mLoadingPackages.iterator();
+ while (loaderIt.hasNext()) {
+ String loader = loaderIt.next();
+ Set<Integer> loadingPackageUsers = packageToUsersMap.get(loader);
+ if (loadingPackageUsers == null
+ || !loadingPackageUsers.contains(fileUserId)) {
+ loaderIt.remove();
+ }
+ }
+ if (fileInfo.mLoadingPackages.isEmpty()) {
+ fileIt.remove();
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Represents a single dynamic code file loaded by one or more packages. Note that it is
+ * possible for one app to dynamically load code from a different app's home dir, if the
+ * owning app:
+ * <ul>
+ * <li>Targets API 27 or lower and has shared its home dir.
+ * <li>Is a system app.
+ * <li>Has a shared UID with the loading app.
+ * </ul>
+ */
+ static class DynamicCodeFile {
+ final char mFileType;
+ final int mUserId;
+ final Set<String> mLoadingPackages;
+
+ private DynamicCodeFile(char type, int user, String... packages) {
+ mFileType = type;
+ mUserId = user;
+ mLoadingPackages = new HashSet<>(Arrays.asList(packages));
+ }
+
+ private DynamicCodeFile(DynamicCodeFile original) {
+ mFileType = original.mFileType;
+ mUserId = original.mUserId;
+ mLoadingPackages = new HashSet<>(original.mLoadingPackages);
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/signedconfig/SignedConfigApplicator.java b/services/core/java/com/android/server/signedconfig/SignedConfigApplicator.java
index 7ce071f..2312f5f 100644
--- a/services/core/java/com/android/server/signedconfig/SignedConfigApplicator.java
+++ b/services/core/java/com/android/server/signedconfig/SignedConfigApplicator.java
@@ -17,12 +17,86 @@
package com.android.server.signedconfig;
import android.content.Context;
+import android.os.Build;
+import android.provider.Settings;
+import android.util.ArraySet;
+import android.util.Slog;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
class SignedConfigApplicator {
- static void applyConfig(Context context, String config, String signature) {
- //TODO verify signature
- //TODO parse & apply config
+ private static final String TAG = "SignedConfig";
+
+ private static final Set<String> ALLOWED_KEYS = Collections.unmodifiableSet(new ArraySet<>(
+ Arrays.asList(
+ Settings.Global.HIDDEN_API_POLICY,
+ Settings.Global.HIDDEN_API_BLACKLIST_EXEMPTIONS
+ )));
+
+ private final Context mContext;
+ private final String mSourcePackage;
+
+ SignedConfigApplicator(Context context, String sourcePackage) {
+ mContext = context;
+ mSourcePackage = sourcePackage;
}
+ private boolean checkSignature(String data, String signature) {
+ Slog.w(TAG, "SIGNATURE CHECK NOT IMPLEMENTED YET!");
+ return false;
+ }
+
+ private int getCurrentConfigVersion() {
+ return Settings.Global.getInt(mContext.getContentResolver(),
+ Settings.Global.SIGNED_CONFIG_VERSION, 0);
+ }
+
+ private void updateCurrentConfig(int version, Map<String, String> values) {
+ for (Map.Entry<String, String> e: values.entrySet()) {
+ Settings.Global.putString(
+ mContext.getContentResolver(),
+ e.getKey(),
+ e.getValue());
+ }
+ Settings.Global.putInt(
+ mContext.getContentResolver(), Settings.Global.SIGNED_CONFIG_VERSION, version);
+ }
+
+
+ void applyConfig(String configStr, String signature) {
+ if (!checkSignature(configStr, signature)) {
+ Slog.e(TAG, "Signature check on signed configuration in package " + mSourcePackage
+ + " failed; ignoring");
+ return;
+ }
+ SignedConfig config;
+ try {
+ config = SignedConfig.parse(configStr, ALLOWED_KEYS);
+ } catch (InvalidConfigException e) {
+ Slog.e(TAG, "Failed to parse config from package " + mSourcePackage, e);
+ return;
+ }
+ int currentVersion = getCurrentConfigVersion();
+ if (currentVersion >= config.version) {
+ Slog.i(TAG, "Config from package " + mSourcePackage + " is older than existing: "
+ + config.version + "<=" + currentVersion);
+ return;
+ }
+ // We have new config!
+ Slog.i(TAG, "Got new signed config from package " + mSourcePackage + ": version "
+ + config.version + " replacing existing version " + currentVersion);
+ SignedConfig.PerSdkConfig matchedConfig =
+ config.getMatchingConfig(Build.VERSION.SDK_INT);
+ if (matchedConfig == null) {
+ Slog.i(TAG, "Config is not applicable to current SDK version; ignoring");
+ return;
+ }
+
+ Slog.i(TAG, "Updating signed config to version " + config.version);
+ updateCurrentConfig(config.version, matchedConfig.values);
+ }
}
diff --git a/services/core/java/com/android/server/signedconfig/SignedConfigService.java b/services/core/java/com/android/server/signedconfig/SignedConfigService.java
index 1485686..be1d41d 100644
--- a/services/core/java/com/android/server/signedconfig/SignedConfigService.java
+++ b/services/core/java/com/android/server/signedconfig/SignedConfigService.java
@@ -85,7 +85,8 @@
Slog.d(TAG, "Got signed config: " + config);
Slog.d(TAG, "Got config signature: " + signature);
}
- SignedConfigApplicator.applyConfig(mContext, config, signature);
+ new SignedConfigApplicator(mContext, packageName).applyConfig(
+ config, signature);
} else {
if (DBG) Slog.d(TAG, "Package has no config/signature.");
}
diff --git a/services/core/java/com/android/server/wm/BarController.java b/services/core/java/com/android/server/wm/BarController.java
index a335fa2..5b20af3 100644
--- a/services/core/java/com/android/server/wm/BarController.java
+++ b/services/core/java/com/android/server/wm/BarController.java
@@ -219,7 +219,7 @@
}
private boolean updateStateLw(final int state) {
- if (state != mState) {
+ if (mWin != null && state != mState) {
mState = state;
if (DEBUG) Slog.d(mTag, "mState: " + StatusBarManager.windowStateToString(state));
mHandler.post(new Runnable() {
diff --git a/services/core/java/com/android/server/wm/InputManagerCallback.java b/services/core/java/com/android/server/wm/InputManagerCallback.java
index 639ed02..f9c9d33 100644
--- a/services/core/java/com/android/server/wm/InputManagerCallback.java
+++ b/services/core/java/com/android/server/wm/InputManagerCallback.java
@@ -1,5 +1,6 @@
package com.android.server.wm;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
@@ -9,7 +10,6 @@
import android.os.Debug;
import android.os.IBinder;
import android.util.Slog;
-import android.view.InputApplicationHandle;
import android.view.KeyEvent;
import android.view.WindowManager;
@@ -204,6 +204,37 @@
+ WindowManagerService.TYPE_LAYER_OFFSET;
}
+ /** Callback to get pointer display id. */
+ @Override
+ public int getPointerDisplayId() {
+ synchronized (mService.mGlobalLock) {
+ // If desktop mode is not enabled, show on the default display.
+ if (!mService.mForceDesktopModeOnExternalDisplays) {
+ return DEFAULT_DISPLAY;
+ }
+
+ // Look for the topmost freeform display.
+ int firstExternalDisplayId = DEFAULT_DISPLAY;
+ for (int i = mService.mRoot.mChildren.size() - 1; i >= 0; --i) {
+ final DisplayContent displayContent = mService.mRoot.mChildren.get(i);
+ // Heuristic solution here. Currently when "Freeform windows" developer option is
+ // enabled we automatically put secondary displays in freeform mode and emulating
+ // "desktop mode". It also makes sense to show the pointer on the same display.
+ if (displayContent.getWindowingMode() == WINDOWING_MODE_FREEFORM) {
+ return displayContent.getDisplayId();
+ }
+
+ if (firstExternalDisplayId == DEFAULT_DISPLAY
+ && displayContent.getDisplayId() != DEFAULT_DISPLAY) {
+ firstExternalDisplayId = displayContent.getDisplayId();
+ }
+ }
+
+ // Look for the topmost non-default display
+ return firstExternalDisplayId;
+ }
+ }
+
/** Waits until the built-in input devices have been configured. */
public boolean waitForInputDevicesReady(long timeoutMillis) {
synchronized (mInputDevicesReadyMonitor) {
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index 43d2dcf..bf83ca9 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 @@
jmethodID getLongPressTimeout;
jmethodID getPointerLayer;
jmethodID getPointerIcon;
+ jmethodID getPointerDisplayId;
jmethodID getKeyboardLayoutOverlay;
jmethodID getDeviceAlias;
jmethodID getTouchCalibrationForInputDevice;
@@ -174,15 +175,6 @@
loadSystemIconAsSpriteWithPointerIcon(env, contextObj, style, &pointerIcon, outSpriteIcon);
}
-static void updatePointerControllerFromViewport(
- sp<PointerController> controller, const DisplayViewport* const viewport) {
- if (controller != nullptr && viewport != nullptr) {
- const int32_t width = viewport->logicalRight - viewport->logicalLeft;
- const int32_t height = viewport->logicalBottom - viewport->logicalTop;
- controller->setDisplayViewport(width, height, viewport->orientation);
- }
-}
-
enum {
WM_ACTION_PASS_TO_USER = 1,
};
@@ -242,6 +234,7 @@
jfloatArray matrixArr);
virtual TouchAffineTransformation getTouchAffineTransformation(
const std::string& inputDeviceDescriptor, int32_t surfaceRotation);
+ virtual void updatePointerDisplay();
/* --- InputDispatcherPolicyInterface implementation --- */
@@ -314,10 +307,11 @@
std::atomic<bool> mInteractive;
- void updateInactivityTimeoutLocked(const sp<PointerController>& controller);
+ void updateInactivityTimeoutLocked();
void handleInterceptActions(jint wmActions, nsecs_t when, uint32_t& policyFlags);
void ensureSpriteControllerLocked();
-
+ const DisplayViewport* findDisplayViewportLocked(int32_t displayId);
+ int32_t getPointerDisplayId();
static bool checkAndClearExceptionFromCallback(JNIEnv* env, const char* methodName);
static inline JNIEnv* jniEnv() {
@@ -391,9 +385,10 @@
return false;
}
-static const DisplayViewport* findInternalViewport(const std::vector<DisplayViewport>& viewports) {
- for (const DisplayViewport& v : viewports) {
- if (v.type == ViewportType::VIEWPORT_INTERNAL) {
+const DisplayViewport* NativeInputManager::findDisplayViewportLocked(int32_t displayId)
+ REQUIRES(mLock) {
+ for (const DisplayViewport& v : mLocked.viewports) {
+ if (v.displayId == displayId) {
return &v;
}
}
@@ -420,20 +415,10 @@
}
}
- const DisplayViewport* newInternalViewport = findInternalViewport(viewports);
- {
+ { // acquire lock
AutoMutex _l(mLock);
- const DisplayViewport* oldInternalViewport = findInternalViewport(mLocked.viewports);
- // Internal viewport has changed if there wasn't one earlier, and there is one now, or,
- // if they are different.
- const bool internalViewportChanged = (newInternalViewport != nullptr) &&
- (oldInternalViewport == nullptr || (*oldInternalViewport != *newInternalViewport));
- if (internalViewportChanged) {
- sp<PointerController> controller = mLocked.pointerController.promote();
- updatePointerControllerFromViewport(controller, newInternalViewport);
- }
mLocked.viewports = viewports;
- }
+ } // release lock
mInputManager->getReader()->requestRefreshConfiguration(
InputReaderConfiguration::CHANGE_DISPLAY_INFO);
@@ -556,15 +541,43 @@
controller = new PointerController(this, mLooper, mLocked.spriteController);
mLocked.pointerController = controller;
-
- const DisplayViewport* internalViewport = findInternalViewport(mLocked.viewports);
- updatePointerControllerFromViewport(controller, internalViewport);
-
- updateInactivityTimeoutLocked(controller);
+ updateInactivityTimeoutLocked();
}
+
return controller;
}
+int32_t NativeInputManager::getPointerDisplayId() {
+ JNIEnv* env = jniEnv();
+ jint pointerDisplayId = env->CallIntMethod(mServiceObj,
+ gServiceClassInfo.getPointerDisplayId);
+ if (checkAndClearExceptionFromCallback(env, "getPointerDisplayId")) {
+ pointerDisplayId = ADISPLAY_ID_DEFAULT;
+ }
+
+ return pointerDisplayId;
+}
+
+void NativeInputManager::updatePointerDisplay() {
+ ATRACE_CALL();
+
+ jint pointerDisplayId = getPointerDisplayId();
+
+ AutoMutex _l(mLock);
+ sp<PointerController> controller = mLocked.pointerController.promote();
+ if (controller != nullptr) {
+ const DisplayViewport* viewport = findDisplayViewportLocked(pointerDisplayId);
+ if (viewport == nullptr) {
+ ALOGW("Can't find pointer display viewport, fallback to default display.");
+ viewport = findDisplayViewportLocked(ADISPLAY_ID_DEFAULT);
+ }
+
+ if (viewport != nullptr) {
+ controller->setDisplayViewport(*viewport);
+ }
+ }
+}
+
void NativeInputManager::ensureSpriteControllerLocked() REQUIRES(mLock) {
if (mLocked.spriteController == nullptr) {
JNIEnv* env = jniEnv();
@@ -821,16 +834,16 @@
if (mLocked.systemUiVisibility != visibility) {
mLocked.systemUiVisibility = visibility;
-
- sp<PointerController> controller = mLocked.pointerController.promote();
- if (controller != nullptr) {
- updateInactivityTimeoutLocked(controller);
- }
+ updateInactivityTimeoutLocked();
}
}
-void NativeInputManager::updateInactivityTimeoutLocked(const sp<PointerController>& controller)
- REQUIRES(mLock) {
+void NativeInputManager::updateInactivityTimeoutLocked() REQUIRES(mLock) {
+ sp<PointerController> controller = mLocked.pointerController.promote();
+ if (controller == nullptr) {
+ return;
+ }
+
bool lightsOut = mLocked.systemUiVisibility & ASYSTEM_UI_VISIBILITY_STATUS_BAR_HIDDEN;
controller->setInactivityTimeout(lightsOut
? PointerController::INACTIVITY_TIMEOUT_SHORT
@@ -1824,6 +1837,9 @@
GET_METHOD_ID(gServiceClassInfo.getPointerIcon, clazz,
"getPointerIcon", "()Landroid/view/PointerIcon;");
+ GET_METHOD_ID(gServiceClassInfo.getPointerDisplayId, clazz,
+ "getPointerDisplayId", "()I");
+
GET_METHOD_ID(gServiceClassInfo.getKeyboardLayoutOverlay, clazz,
"getKeyboardLayoutOverlay",
"(Landroid/hardware/input/InputDeviceIdentifier;)[Ljava/lang/String;");
diff --git a/services/tests/servicestests/src/com/android/server/pm/dex/DexManagerTests.java b/services/tests/servicestests/src/com/android/server/pm/dex/DexManagerTests.java
index dad7b93..fd07cb0 100644
--- a/services/tests/servicestests/src/com/android/server/pm/dex/DexManagerTests.java
+++ b/services/tests/servicestests/src/com/android/server/pm/dex/DexManagerTests.java
@@ -18,10 +18,14 @@
import static com.android.server.pm.dex.PackageDexUsage.DexUseInfo;
import static com.android.server.pm.dex.PackageDexUsage.PackageUseInfo;
+import static com.android.server.pm.dex.PackageDynamicCodeLoading.DynamicCodeFile;
+import static com.android.server.pm.dex.PackageDynamicCodeLoading.PackageDynamicCode;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
@@ -41,6 +45,10 @@
import com.android.server.pm.Installer;
+import dalvik.system.DelegateLastClassLoader;
+import dalvik.system.PathClassLoader;
+import dalvik.system.VMRuntime;
+
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -50,10 +58,6 @@
import org.mockito.junit.MockitoRule;
import org.mockito.quality.Strictness;
-import dalvik.system.DelegateLastClassLoader;
-import dalvik.system.PathClassLoader;
-import dalvik.system.VMRuntime;
-
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
@@ -129,6 +133,9 @@
// Package is not used by others, so we should get nothing back.
assertNoUseInfo(mFooUser0);
+
+ // A package loading its own code is not stored as DCL.
+ assertNoDclInfo(mFooUser0);
}
@Test
@@ -140,6 +147,8 @@
PackageUseInfo pui = getPackageUseInfo(mBarUser0);
assertIsUsedByOtherApps(mBarUser0, pui, true);
assertTrue(pui.getDexUseInfoMap().isEmpty());
+
+ assertHasDclInfo(mBarUser0, mFooUser0, mBarUser0.getBaseAndSplitDexPaths());
}
@Test
@@ -152,6 +161,8 @@
assertIsUsedByOtherApps(mFooUser0, pui, false);
assertEquals(fooSecondaries.size(), pui.getDexUseInfoMap().size());
assertSecondaryUse(mFooUser0, pui, fooSecondaries, /*isUsedByOtherApps*/false, mUser0);
+
+ assertHasDclInfo(mFooUser0, mFooUser0, fooSecondaries);
}
@Test
@@ -164,6 +175,8 @@
assertIsUsedByOtherApps(mBarUser0, pui, false);
assertEquals(barSecondaries.size(), pui.getDexUseInfoMap().size());
assertSecondaryUse(mFooUser0, pui, barSecondaries, /*isUsedByOtherApps*/true, mUser0);
+
+ assertHasDclInfo(mBarUser0, mFooUser0, barSecondaries);
}
@Test
@@ -200,9 +213,10 @@
}
@Test
- public void testPackageUseInfoNotFound() {
+ public void testNoNotify() {
// Assert we don't get back data we did not previously record.
assertNoUseInfo(mFooUser0);
+ assertNoDclInfo(mFooUser0);
}
@Test
@@ -210,6 +224,7 @@
// Notifying with an invalid ISA should be ignored.
notifyDexLoad(mInvalidIsa, mInvalidIsa.getSecondaryDexPaths(), mUser0);
assertNoUseInfo(mInvalidIsa);
+ assertNoDclInfo(mInvalidIsa);
}
@Test
@@ -218,6 +233,7 @@
// register in DexManager#load should be ignored.
notifyDexLoad(mDoesNotExist, mDoesNotExist.getBaseAndSplitDexPaths(), mUser0);
assertNoUseInfo(mDoesNotExist);
+ assertNoDclInfo(mDoesNotExist);
}
@Test
@@ -226,6 +242,8 @@
// Request should be ignored.
notifyDexLoad(mBarUser1, mBarUser0.getSecondaryDexPaths(), mUser1);
assertNoUseInfo(mBarUser1);
+
+ assertNoDclInfo(mBarUser1);
}
@Test
@@ -235,6 +253,10 @@
// still check that nothing goes unexpected in DexManager.
notifyDexLoad(mBarUser0, mFooUser0.getBaseAndSplitDexPaths(), mUser1);
assertNoUseInfo(mBarUser1);
+ assertNoUseInfo(mFooUser0);
+
+ assertNoDclInfo(mBarUser1);
+ assertNoDclInfo(mFooUser0);
}
@Test
@@ -247,6 +269,7 @@
// is trying to load something from it we should not find it.
notifyDexLoad(mFooUser0, newSecondaries, mUser0);
assertNoUseInfo(newPackage);
+ assertNoDclInfo(newPackage);
// Notify about newPackage install and let mFoo load its dexes.
mDexManager.notifyPackageInstalled(newPackage.mPackageInfo, mUser0);
@@ -257,6 +280,7 @@
assertIsUsedByOtherApps(newPackage, pui, false);
assertEquals(newSecondaries.size(), pui.getDexUseInfoMap().size());
assertSecondaryUse(newPackage, pui, newSecondaries, /*isUsedByOtherApps*/true, mUser0);
+ assertHasDclInfo(newPackage, mFooUser0, newSecondaries);
}
@Test
@@ -273,6 +297,7 @@
assertIsUsedByOtherApps(newPackage, pui, false);
assertEquals(newSecondaries.size(), pui.getDexUseInfoMap().size());
assertSecondaryUse(newPackage, pui, newSecondaries, /*isUsedByOtherApps*/false, mUser0);
+ assertHasDclInfo(newPackage, newPackage, newSecondaries);
}
@Test
@@ -305,6 +330,7 @@
// We shouldn't find yet the new split as we didn't notify the package update.
notifyDexLoad(mFooUser0, newSplits, mUser0);
assertNoUseInfo(mBarUser0);
+ assertNoDclInfo(mBarUser0);
// Notify that bar is updated. splitSourceDirs will contain the updated path.
mDexManager.notifyPackageUpdated(mBarUser0.getPackageName(),
@@ -314,8 +340,8 @@
// Now, when the split is loaded we will find it and we should mark Bar as usedByOthers.
notifyDexLoad(mFooUser0, newSplits, mUser0);
PackageUseInfo pui = getPackageUseInfo(mBarUser0);
- assertNotNull(pui);
assertIsUsedByOtherApps(newSplits, pui, true);
+ assertHasDclInfo(mBarUser0, mFooUser0, newSplits);
}
@Test
@@ -326,11 +352,15 @@
mDexManager.notifyPackageDataDestroyed(mBarUser0.getPackageName(), mUser0);
- // Bar should not be around since it was removed for all users.
+ // Data for user 1 should still be present
PackageUseInfo pui = getPackageUseInfo(mBarUser1);
- assertNotNull(pui);
assertSecondaryUse(mBarUser1, pui, mBarUser1.getSecondaryDexPaths(),
/*isUsedByOtherApps*/false, mUser1);
+ assertHasDclInfo(mBarUser1, mBarUser1, mBarUser1.getSecondaryDexPaths());
+
+ // But not user 0
+ assertNoUseInfo(mBarUser0, mUser0);
+ assertNoDclInfo(mBarUser0, mUser0);
}
@Test
@@ -349,6 +379,8 @@
PackageUseInfo pui = getPackageUseInfo(mFooUser0);
assertIsUsedByOtherApps(mFooUser0, pui, true);
assertTrue(pui.getDexUseInfoMap().isEmpty());
+
+ assertNoDclInfo(mFooUser0);
}
@Test
@@ -362,6 +394,7 @@
// Foo should not be around since all its secondary dex info were deleted
// and it is not used by other apps.
assertNoUseInfo(mFooUser0);
+ assertNoDclInfo(mFooUser0);
}
@Test
@@ -374,6 +407,7 @@
// Bar should not be around since it was removed for all users.
assertNoUseInfo(mBarUser0);
+ assertNoDclInfo(mBarUser0);
}
@Test
@@ -383,6 +417,7 @@
notifyDexLoad(mFooUser0, Arrays.asList(frameworkDex), mUser0);
// The dex file should not be recognized as a package.
assertFalse(mDexManager.hasInfoOnPackage(frameworkDex));
+ assertNull(mDexManager.getPackageDynamicCodeInfo(frameworkDex));
}
@Test
@@ -395,6 +430,8 @@
assertIsUsedByOtherApps(mFooUser0, pui, false);
assertEquals(fooSecondaries.size(), pui.getDexUseInfoMap().size());
assertSecondaryUse(mFooUser0, pui, fooSecondaries, /*isUsedByOtherApps*/false, mUser0);
+
+ assertHasDclInfo(mFooUser0, mFooUser0, fooSecondaries);
}
@Test
@@ -402,7 +439,12 @@
List<String> secondaries = mBarUser0UnsupportedClassLoader.getSecondaryDexPaths();
notifyDexLoad(mBarUser0UnsupportedClassLoader, secondaries, mUser0);
+ // We don't record the dex usage
assertNoUseInfo(mBarUser0UnsupportedClassLoader);
+
+ // But we do record this as an intance of dynamic code loading
+ assertHasDclInfo(
+ mBarUser0UnsupportedClassLoader, mBarUser0UnsupportedClassLoader, secondaries);
}
@Test
@@ -414,6 +456,8 @@
notifyDexLoad(mBarUser0, classLoaders, classPaths, mUser0);
assertNoUseInfo(mBarUser0);
+
+ assertHasDclInfo(mBarUser0, mBarUser0, mBarUser0.getSecondaryDexPaths());
}
@Test
@@ -421,6 +465,7 @@
notifyDexLoad(mBarUser0, null, mUser0);
assertNoUseInfo(mBarUser0);
+ assertNoDclInfo(mBarUser0);
}
@Test
@@ -455,12 +500,14 @@
notifyDexLoad(mBarUser0, secondaries, mUser0);
PackageUseInfo pui = getPackageUseInfo(mBarUser0);
assertSecondaryUse(mBarUser0, pui, secondaries, /*isUsedByOtherApps*/false, mUser0);
+ assertHasDclInfo(mBarUser0, mBarUser0, secondaries);
// Record bar secondaries again with an unsupported class loader. This should not change the
// context.
notifyDexLoad(mBarUser0UnsupportedClassLoader, secondaries, mUser0);
pui = getPackageUseInfo(mBarUser0);
assertSecondaryUse(mBarUser0, pui, secondaries, /*isUsedByOtherApps*/false, mUser0);
+ assertHasDclInfo(mBarUser0, mBarUser0, secondaries);
}
@Test
@@ -533,13 +580,53 @@
private PackageUseInfo getPackageUseInfo(TestData testData) {
assertTrue(mDexManager.hasInfoOnPackage(testData.getPackageName()));
- return mDexManager.getPackageUseInfoOrDefault(testData.getPackageName());
+ PackageUseInfo pui = mDexManager.getPackageUseInfoOrDefault(testData.getPackageName());
+ assertNotNull(pui);
+ return pui;
}
private void assertNoUseInfo(TestData testData) {
assertFalse(mDexManager.hasInfoOnPackage(testData.getPackageName()));
}
+ private void assertNoUseInfo(TestData testData, int userId) {
+ if (!mDexManager.hasInfoOnPackage(testData.getPackageName())) {
+ return;
+ }
+ PackageUseInfo pui = getPackageUseInfo(testData);
+ for (DexUseInfo dexUseInfo : pui.getDexUseInfoMap().values()) {
+ assertNotEquals(userId, dexUseInfo.getOwnerUserId());
+ }
+ }
+
+ private void assertNoDclInfo(TestData testData) {
+ assertNull(mDexManager.getPackageDynamicCodeInfo(testData.getPackageName()));
+ }
+
+ private void assertNoDclInfo(TestData testData, int userId) {
+ PackageDynamicCode info = mDexManager.getPackageDynamicCodeInfo(testData.getPackageName());
+ if (info == null) {
+ return;
+ }
+
+ for (DynamicCodeFile fileInfo : info.mFileUsageMap.values()) {
+ assertNotEquals(userId, fileInfo.mUserId);
+ }
+ }
+
+ private void assertHasDclInfo(TestData owner, TestData loader, List<String> paths) {
+ PackageDynamicCode info = mDexManager.getPackageDynamicCodeInfo(owner.getPackageName());
+ assertNotNull("No DCL data for owner " + owner.getPackageName(), info);
+ for (String path : paths) {
+ DynamicCodeFile fileInfo = info.mFileUsageMap.get(path);
+ assertNotNull("No DCL data for path " + path, fileInfo);
+ assertEquals(PackageDynamicCodeLoading.FILE_TYPE_DEX, fileInfo.mFileType);
+ assertEquals(owner.mUserId, fileInfo.mUserId);
+ assertTrue("No DCL data for loader " + loader.getPackageName(),
+ fileInfo.mLoadingPackages.contains(loader.getPackageName()));
+ }
+ }
+
private static PackageInfo getMockPackageInfo(String packageName, int userId) {
PackageInfo pi = new PackageInfo();
pi.packageName = packageName;
@@ -563,11 +650,13 @@
private final PackageInfo mPackageInfo;
private final String mLoaderIsa;
private final String mClassLoader;
+ private final int mUserId;
private TestData(String packageName, String loaderIsa, int userId, String classLoader) {
mPackageInfo = getMockPackageInfo(packageName, userId);
mLoaderIsa = loaderIsa;
mClassLoader = classLoader;
+ mUserId = userId;
}
private TestData(String packageName, String loaderIsa, int userId) {
@@ -603,9 +692,7 @@
List<String> getBaseAndSplitDexPaths() {
List<String> paths = new ArrayList<>();
paths.add(mPackageInfo.applicationInfo.sourceDir);
- for (String split : mPackageInfo.applicationInfo.splitSourceDirs) {
- paths.add(split);
- }
+ Collections.addAll(paths, mPackageInfo.applicationInfo.splitSourceDirs);
return paths;
}
diff --git a/services/tests/servicestests/src/com/android/server/pm/dex/PackageDynamicCodeLoadingTests.java b/services/tests/servicestests/src/com/android/server/pm/dex/PackageDynamicCodeLoadingTests.java
new file mode 100644
index 0000000..eb4cc4e
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/pm/dex/PackageDynamicCodeLoadingTests.java
@@ -0,0 +1,560 @@
+/*
+ * Copyright 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.pm.dex;
+
+import static com.android.server.pm.dex.PackageDynamicCodeLoading.escape;
+import static com.android.server.pm.dex.PackageDynamicCodeLoading.unescape;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertThrows;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.pm.dex.PackageDynamicCodeLoading.DynamicCodeFile;
+import com.android.server.pm.dex.PackageDynamicCodeLoading.PackageDynamicCode;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class PackageDynamicCodeLoadingTests {
+
+ // Deliberately making a copy here since we're testing identity and
+ // string literals have a tendency to be identical.
+ private static final String TRIVIAL_STRING = new String("hello/world");
+ private static final Entry[] NO_ENTRIES = {};
+ private static final String[] NO_PACKAGES = {};
+
+ @Test
+ public void testRecord() {
+ Entry[] entries = {
+ new Entry("owning.package1", "/path/file1", 'D', 10, "loading.package1"),
+ new Entry("owning.package1", "/path/file1", 'D', 10, "loading.package2"),
+ new Entry("owning.package1", "/path/file2", 'D', 5, "loading.package1"),
+ new Entry("owning.package2", "/path/file3", 'D', 0, "loading.package2"),
+ };
+
+ PackageDynamicCodeLoading info = makePackageDcl(entries);
+ assertHasEntries(info, entries);
+ }
+
+ @Test
+ public void testRecord_returnsHasChanged() {
+ Entry owner1Path1Loader1 =
+ new Entry("owning.package1", "/path/file1", 'D', 10, "loading.package1");
+ Entry owner1Path1Loader2 =
+ new Entry("owning.package1", "/path/file1", 'D', 10, "loading.package2");
+ Entry owner1Path2Loader1 =
+ new Entry("owning.package1", "/path/file2", 'D', 10, "loading.package1");
+ Entry owner2Path1Loader1 =
+ new Entry("owning.package2", "/path/file1", 'D', 10, "loading.package2");
+
+ PackageDynamicCodeLoading info = new PackageDynamicCodeLoading();
+
+ assertTrue(record(info, owner1Path1Loader1));
+ assertFalse(record(info, owner1Path1Loader1));
+
+ assertTrue(record(info, owner1Path1Loader2));
+ assertFalse(record(info, owner1Path1Loader2));
+
+ assertTrue(record(info, owner1Path2Loader1));
+ assertFalse(record(info, owner1Path2Loader1));
+
+ assertTrue(record(info, owner2Path1Loader1));
+ assertFalse(record(info, owner2Path1Loader1));
+
+ assertHasEntries(info,
+ owner1Path1Loader1, owner1Path1Loader2, owner1Path2Loader1, owner2Path1Loader1);
+ }
+
+ @Test
+ public void testRecord_changeUserForFile_throws() {
+ Entry entry1 = new Entry("owning.package1", "/path/file1", 'D', 10, "loading.package1");
+ Entry entry2 = new Entry("owning.package1", "/path/file1", 'D', 20, "loading.package1");
+
+ PackageDynamicCodeLoading info = makePackageDcl(entry1);
+
+ assertThrows(() -> record(info, entry2));
+ assertHasEntries(info, entry1);
+ }
+
+ @Test
+ public void testRecord_badFileType_throws() {
+ Entry entry = new Entry("owning.package", "/path/file", 'Z', 10, "loading.package");
+ PackageDynamicCodeLoading info = new PackageDynamicCodeLoading();
+
+ assertThrows(() -> record(info, entry));
+ }
+
+ @Test
+ public void testClear() {
+ Entry[] entries = {
+ new Entry("owner1", "file1", 'D', 10, "loader1"),
+ new Entry("owner2", "file2", 'D', 20, "loader2"),
+ };
+ PackageDynamicCodeLoading info = makePackageDcl(entries);
+
+ info.clear();
+ assertHasEntries(info, NO_ENTRIES);
+ }
+
+ @Test
+ public void testRemovePackage_present() {
+ Entry other = new Entry("other", "file", 'D', 0, "loader");
+ Entry[] entries = {
+ new Entry("owner", "file1", 'D', 10, "loader1"),
+ new Entry("owner", "file2", 'D', 20, "loader2"),
+ other
+ };
+ PackageDynamicCodeLoading info = makePackageDcl(entries);
+
+ assertTrue(info.removePackage("owner"));
+ assertHasEntries(info, other);
+ assertHasPackages(info, "other");
+ }
+
+ @Test
+ public void testRemovePackage_notPresent() {
+ Entry[] entries = { new Entry("owner", "file", 'D', 0, "loader") };
+ PackageDynamicCodeLoading info = makePackageDcl(entries);
+
+ assertFalse(info.removePackage("other"));
+ assertHasEntries(info, entries);
+ }
+
+ @Test
+ public void testRemoveUserPackage_notPresent() {
+ Entry[] entries = { new Entry("owner", "file", 'D', 0, "loader") };
+ PackageDynamicCodeLoading info = makePackageDcl(entries);
+
+ assertFalse(info.removeUserPackage("other", 0));
+ assertHasEntries(info, entries);
+ }
+
+ @Test
+ public void testRemoveUserPackage_presentWithNoOtherUsers() {
+ Entry other = new Entry("other", "file", 'D', 0, "loader");
+ Entry[] entries = {
+ new Entry("owner", "file1", 'D', 0, "loader1"),
+ new Entry("owner", "file2", 'D', 0, "loader2"),
+ other
+ };
+ PackageDynamicCodeLoading info = makePackageDcl(entries);
+
+ assertTrue(info.removeUserPackage("owner", 0));
+ assertHasEntries(info, other);
+ assertHasPackages(info, "other");
+ }
+
+ @Test
+ public void testRemoveUserPackage_presentWithUsers() {
+ Entry other = new Entry("owner", "file", 'D', 1, "loader");
+ Entry[] entries = {
+ new Entry("owner", "file1", 'D', 0, "loader1"),
+ new Entry("owner", "file2", 'D', 0, "loader2"),
+ other
+ };
+ PackageDynamicCodeLoading info = makePackageDcl(entries);
+
+ assertTrue(info.removeUserPackage("owner", 0));
+ assertHasEntries(info, other);
+ }
+
+ @Test
+ public void testRemoveFile_present() {
+ Entry[] entries = {
+ new Entry("package1", "file1", 'D', 0, "loader1"),
+ new Entry("package1", "file2", 'D', 0, "loader1"),
+ new Entry("package2", "file1", 'D', 0, "loader2"),
+ };
+ Entry[] expectedSurvivors = {
+ new Entry("package1", "file2", 'D', 0, "loader1"),
+ new Entry("package2", "file1", 'D', 0, "loader2"),
+ };
+ PackageDynamicCodeLoading info = makePackageDcl(entries);
+
+ assertTrue(info.removeFile("package1", "file1", 0));
+ assertHasEntries(info, expectedSurvivors);
+ }
+
+ @Test
+ public void testRemoveFile_onlyEntry() {
+ Entry[] entries = {
+ new Entry("package1", "file1", 'D', 0, "loader1"),
+ };
+ PackageDynamicCodeLoading info = makePackageDcl(entries);
+
+ assertTrue(info.removeFile("package1", "file1", 0));
+ assertHasEntries(info, NO_ENTRIES);
+ assertHasPackages(info, NO_PACKAGES);
+ }
+
+ @Test
+ public void testRemoveFile_notPresent() {
+ Entry[] entries = {
+ new Entry("package1", "file2", 'D', 0, "loader1"),
+ };
+ PackageDynamicCodeLoading info = makePackageDcl(entries);
+
+ assertFalse(info.removeFile("package1", "file1", 0));
+ assertHasEntries(info, entries);
+ }
+
+ @Test
+ public void testRemoveFile_wrongUser() {
+ Entry[] entries = {
+ new Entry("package1", "file1", 'D', 10, "loader1"),
+ };
+ PackageDynamicCodeLoading info = makePackageDcl(entries);
+
+ assertFalse(info.removeFile("package1", "file1", 0));
+ assertHasEntries(info, entries);
+ }
+
+ @Test
+ public void testSyncData() {
+ Map<String, Set<Integer>> packageToUsersMap = ImmutableMap.of(
+ "package1", ImmutableSet.of(10, 20),
+ "package2", ImmutableSet.of(20));
+
+ Entry[] entries = {
+ new Entry("deleted.packaged", "file1", 'D', 10, "package1"),
+ new Entry("package1", "file2", 'D', 20, "package2"),
+ new Entry("package1", "file3", 'D', 10, "package2"),
+ new Entry("package1", "file3", 'D', 10, "deleted.package"),
+ new Entry("package2", "file4", 'D', 20, "deleted.package"),
+ };
+
+ Entry[] expectedSurvivors = {
+ new Entry("package1", "file2", 'D', 20, "package2"),
+ };
+
+ PackageDynamicCodeLoading info = makePackageDcl(entries);
+ info.syncData(packageToUsersMap);
+ assertHasEntries(info, expectedSurvivors);
+ assertHasPackages(info, "package1");
+ }
+
+ @Test
+ public void testRead_onlyHeader_emptyResult() throws Exception {
+ assertHasEntries(read("DCL1"), NO_ENTRIES);
+ }
+
+ @Test
+ public void testRead_noHeader_throws() {
+ assertThrows(IOException.class, () -> read(""));
+ }
+
+ @Test
+ public void testRead_wrongHeader_throws() {
+ assertThrows(IOException.class, () -> read("DCL2"));
+ }
+
+ @Test
+ public void testRead_oneEntry() throws Exception {
+ String inputText = ""
+ + "DCL1\n"
+ + "P:owning.package\n"
+ + "D:10:loading.package:/path/fi\\\\le\n";
+ assertHasEntries(read(inputText),
+ new Entry("owning.package", "/path/fi\\le", 'D', 10, "loading.package"));
+ }
+
+ @Test
+ public void testRead_emptyPackage() throws Exception {
+ String inputText = ""
+ + "DCL1\n"
+ + "P:owning.package\n";
+ PackageDynamicCodeLoading info = read(inputText);
+ assertHasEntries(info, NO_ENTRIES);
+ assertHasPackages(info, NO_PACKAGES);
+ }
+
+ @Test
+ public void testRead_complex() throws Exception {
+ String inputText = ""
+ + "DCL1\n"
+ + "P:owning.package1\n"
+ + "D:10:loading.package1,loading.package2:/path/file1\n"
+ + "D:5:loading.package1:/path/file2\n"
+ + "P:owning.package2\n"
+ + "D:0:loading.package2:/path/file3";
+ assertHasEntries(read(inputText),
+ new Entry("owning.package1", "/path/file1", 'D', 10, "loading.package1"),
+ new Entry("owning.package1", "/path/file1", 'D', 10, "loading.package2"),
+ new Entry("owning.package1", "/path/file2", 'D', 5, "loading.package1"),
+ new Entry("owning.package2", "/path/file3", 'D', 0, "loading.package2"));
+ }
+
+ @Test
+ public void testRead_missingPackageLine_throws() {
+ String inputText = ""
+ + "DCL1\n"
+ + "D:10:loading.package:/path/file\n";
+ assertThrows(IOException.class, () -> read(inputText));
+ }
+
+ @Test
+ public void testRead_malformedFile_throws() {
+ String inputText = ""
+ + "DCL1\n"
+ + "P:owning.package\n"
+ + "Hello world!\n";
+ assertThrows(IOException.class, () -> read(inputText));
+ }
+
+ @Test
+ public void testRead_badFileType_throws() {
+ String inputText = ""
+ + "DCL1\n"
+ + "P:owning.package\n"
+ + "X:10:loading.package:/path/file\n";
+ assertThrows(IOException.class, () -> read(inputText));
+ }
+
+ @Test
+ public void testRead_badUserId_throws() {
+ String inputText = ""
+ + "DCL1\n"
+ + "P:owning.package\n"
+ + "D:999999999999999999:loading.package:/path/file\n";
+ assertThrows(IOException.class, () -> read(inputText));
+ }
+
+ @Test
+ public void testRead_missingPackages_throws() {
+ String inputText = ""
+ + "DCL1\n"
+ + "P:owning.package\n"
+ + "D:1:,:/path/file\n";
+ assertThrows(IOException.class, () -> read(inputText));
+ }
+
+ @Test
+ public void testWrite_empty() throws Exception {
+ assertEquals("DCL1\n", write(NO_ENTRIES));
+ }
+
+ @Test
+ public void testWrite_oneEntry() throws Exception {
+ String expected = ""
+ + "DCL1\n"
+ + "P:owning.package\n"
+ + "D:10:loading.package:/path/fi\\\\le\n";
+ String actual = write(
+ new Entry("owning.package", "/path/fi\\le", 'D', 10, "loading.package"));
+ assertEquals(expected, actual);
+ }
+
+ @Test
+ public void testWrite_complex_roundTrips() throws Exception {
+ // There isn't a canonical order for the output in the presence of multiple items.
+ // So we just check that if we read back what we write we end up where we started.
+ Entry[] entries = {
+ new Entry("owning.package1", "/path/file1", 'D', 10, "loading.package1"),
+ new Entry("owning.package1", "/path/file1", 'D', 10, "loading.package2"),
+ new Entry("owning.package1", "/path/file2", 'D', 5, "loading.package1"),
+ new Entry("owning.package2", "/path/fi\\le3", 'D', 0, "loading.package2")
+ };
+ assertHasEntries(read(write(entries)), entries);
+ }
+
+ @Test
+ public void testWrite_failure_throws() {
+ PackageDynamicCodeLoading info = makePackageDcl(
+ new Entry("owning.package", "/path/fi\\le", 'D', 10, "loading.package"));
+ assertThrows(IOException.class, () -> info.write(new ThrowingOutputStream()));
+ }
+
+ @Test
+ public void testEscape_trivialCase_returnsSameString() {
+ assertSame(TRIVIAL_STRING, escape(TRIVIAL_STRING));
+ }
+
+ @Test
+ public void testEscape() {
+ String input = "backslash\\newline\nreturn\r";
+ String expected = "backslash\\\\newline\\nreturn\\r";
+ assertEquals(expected, escape(input));
+ }
+
+ @Test
+ public void testUnescape_trivialCase_returnsSameString() throws Exception {
+ assertSame(TRIVIAL_STRING, unescape(TRIVIAL_STRING));
+ }
+
+ @Test
+ public void testUnescape() throws Exception {
+ String input = "backslash\\\\newline\\nreturn\\r";
+ String expected = "backslash\\newline\nreturn\r";
+ assertEquals(expected, unescape(input));
+ }
+
+ @Test
+ public void testUnescape_badEscape_throws() {
+ assertThrows(IOException.class, () -> unescape("this is \\bad"));
+ }
+
+ @Test
+ public void testUnescape_trailingBackslash_throws() {
+ assertThrows(IOException.class, () -> unescape("don't do this\\"));
+ }
+
+ @Test
+ public void testEscapeUnescape_roundTrips() throws Exception {
+ assertRoundTripsWithEscape("foo");
+ assertRoundTripsWithEscape("\\\\\n\n\r");
+ assertRoundTripsWithEscape("\\a\\b\\");
+ assertRoundTripsWithEscape("\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\");
+ }
+
+ private void assertRoundTripsWithEscape(String original) throws Exception {
+ assertEquals(original, unescape(escape(original)));
+ }
+
+ private boolean record(PackageDynamicCodeLoading info, Entry entry) {
+ return info.record(entry.mOwningPackage, entry.mPath, entry.mFileType, entry.mUserId,
+ entry.mLoadingPackage);
+ }
+
+ private PackageDynamicCodeLoading read(String inputText) throws Exception {
+ ByteArrayInputStream inputStream =
+ new ByteArrayInputStream(inputText.getBytes(UTF_8));
+
+ PackageDynamicCodeLoading info = new PackageDynamicCodeLoading();
+ info.read(inputStream);
+
+ return info;
+ }
+
+ private String write(Entry... entries) throws Exception {
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+ makePackageDcl(entries).write(output);
+ return new String(output.toByteArray(), UTF_8);
+ }
+
+ private Set<Entry> entriesFrom(PackageDynamicCodeLoading info) {
+ ImmutableSet.Builder<Entry> entries = ImmutableSet.builder();
+ for (String owningPackage : info.getAllPackagesWithDynamicCodeLoading()) {
+ PackageDynamicCode packageInfo = info.getPackageDynamicCodeInfo(owningPackage);
+ Map<String, DynamicCodeFile> usageMap = packageInfo.mFileUsageMap;
+ for (Map.Entry<String, DynamicCodeFile> fileEntry : usageMap.entrySet()) {
+ String path = fileEntry.getKey();
+ DynamicCodeFile fileInfo = fileEntry.getValue();
+ for (String loadingPackage : fileInfo.mLoadingPackages) {
+ entries.add(new Entry(owningPackage, path, fileInfo.mFileType, fileInfo.mUserId,
+ loadingPackage));
+ }
+ }
+ }
+
+ return entries.build();
+ }
+
+ private PackageDynamicCodeLoading makePackageDcl(Entry... entries) {
+ PackageDynamicCodeLoading result = new PackageDynamicCodeLoading();
+ for (Entry entry : entries) {
+ result.record(entry.mOwningPackage, entry.mPath, entry.mFileType, entry.mUserId,
+ entry.mLoadingPackage);
+ }
+ return result;
+
+ }
+
+ private void assertHasEntries(PackageDynamicCodeLoading info, Entry... expected) {
+ assertEquals(ImmutableSet.copyOf(expected), entriesFrom(info));
+ }
+
+ private void assertHasPackages(PackageDynamicCodeLoading info, String... expected) {
+ assertEquals(ImmutableSet.copyOf(expected), info.getAllPackagesWithDynamicCodeLoading());
+ }
+
+ /**
+ * Immutable representation of one entry in the dynamic code loading data (one package
+ * owning one file loaded by one package). Has well-behaved equality, hash and toString
+ * for ease of use in assertions.
+ */
+ private static class Entry {
+ private final String mOwningPackage;
+ private final String mPath;
+ private final char mFileType;
+ private final int mUserId;
+ private final String mLoadingPackage;
+
+ private Entry(String owningPackage, String path, char fileType, int userId,
+ String loadingPackage) {
+ mOwningPackage = owningPackage;
+ mPath = path;
+ mFileType = fileType;
+ mUserId = userId;
+ mLoadingPackage = loadingPackage;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Entry that = (Entry) o;
+ return mFileType == that.mFileType
+ && mUserId == that.mUserId
+ && Objects.equals(mOwningPackage, that.mOwningPackage)
+ && Objects.equals(mPath, that.mPath)
+ && Objects.equals(mLoadingPackage, that.mLoadingPackage);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mOwningPackage, mPath, mFileType, mUserId, mLoadingPackage);
+ }
+
+ @Override
+ public String toString() {
+ return "Entry("
+ + "\"" + mOwningPackage + '"'
+ + ", \"" + mPath + '"'
+ + ", '" + mFileType + '\''
+ + ", " + mUserId
+ + ", \"" + mLoadingPackage + '\"'
+ + ')';
+ }
+ }
+
+ private static class ThrowingOutputStream extends OutputStream {
+ @Override
+ public void write(int b) throws IOException {
+ throw new IOException("Intentional failure");
+ }
+ }
+}
diff --git a/tools/hiddenapi/exclude.sh b/tools/hiddenapi/exclude.sh
index 2291e5a..4ffcf68 100755
--- a/tools/hiddenapi/exclude.sh
+++ b/tools/hiddenapi/exclude.sh
@@ -11,6 +11,7 @@
android.system \
com.android.bouncycastle \
com.android.conscrypt \
+ com.android.i18n.phonenumbers \
com.android.okhttp \
com.sun \
dalvik \