Merge "Readd missing null check"
diff --git a/api/current.txt b/api/current.txt
index bc41d65..6da09e3 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -1234,6 +1234,7 @@
field public static final int summaryOff = 16843248; // 0x10101f0
field public static final int summaryOn = 16843247; // 0x10101ef
field public static final int supportsAssist = 16844016; // 0x10104f0
+ field public static final int supportsDismissingWindow = 16844104; // 0x1010548
field public static final int supportsLaunchVoiceAssistFromKeyguard = 16844017; // 0x10104f1
field public static final int supportsLocalInteraction = 16844047; // 0x101050f
field public static final int supportsPictureInPicture = 16844023; // 0x10104f7
@@ -8959,6 +8960,7 @@
field public static final java.lang.String EXTRA_CHOOSER_TARGETS = "android.intent.extra.CHOOSER_TARGETS";
field public static final java.lang.String EXTRA_CHOSEN_COMPONENT = "android.intent.extra.CHOSEN_COMPONENT";
field public static final java.lang.String EXTRA_CHOSEN_COMPONENT_INTENT_SENDER = "android.intent.extra.CHOSEN_COMPONENT_INTENT_SENDER";
+ field public static final java.lang.String EXTRA_CONTENT_ANNOTATIONS = "android.intent.extra.CONTENT_ANNOTATIONS";
field public static final java.lang.String EXTRA_DATA_REMOVED = "android.intent.extra.DATA_REMOVED";
field public static final java.lang.String EXTRA_DOCK_STATE = "android.intent.extra.DOCK_STATE";
field public static final int EXTRA_DOCK_STATE_CAR = 2; // 0x2
diff --git a/api/system-current.txt b/api/system-current.txt
index 24f9aad..5640eaf 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -1347,6 +1347,7 @@
field public static final int summaryOff = 16843248; // 0x10101f0
field public static final int summaryOn = 16843247; // 0x10101ef
field public static final int supportsAssist = 16844016; // 0x10104f0
+ field public static final int supportsDismissingWindow = 16844104; // 0x1010548
field public static final int supportsLaunchVoiceAssistFromKeyguard = 16844017; // 0x10104f1
field public static final int supportsLocalInteraction = 16844047; // 0x101050f
field public static final int supportsPictureInPicture = 16844023; // 0x10104f7
@@ -9335,6 +9336,7 @@
field public static final java.lang.String EXTRA_CHOOSER_TARGETS = "android.intent.extra.CHOOSER_TARGETS";
field public static final java.lang.String EXTRA_CHOSEN_COMPONENT = "android.intent.extra.CHOSEN_COMPONENT";
field public static final java.lang.String EXTRA_CHOSEN_COMPONENT_INTENT_SENDER = "android.intent.extra.CHOSEN_COMPONENT_INTENT_SENDER";
+ field public static final java.lang.String EXTRA_CONTENT_ANNOTATIONS = "android.intent.extra.CONTENT_ANNOTATIONS";
field public static final java.lang.String EXTRA_DATA_REMOVED = "android.intent.extra.DATA_REMOVED";
field public static final java.lang.String EXTRA_DOCK_STATE = "android.intent.extra.DOCK_STATE";
field public static final int EXTRA_DOCK_STATE_CAR = 2; // 0x2
@@ -41395,6 +41397,8 @@
method public java.lang.String getDeviceSoftwareVersion();
method public java.lang.String getGroupIdLevel1();
method public java.lang.String getIccAuthentication(int, int, java.lang.String);
+ method public java.lang.String getImei();
+ method public java.lang.String getImei(int);
method public java.lang.String getLine1Number();
method public java.lang.String getMmsUAProfUrl();
method public java.lang.String getMmsUserAgent();
diff --git a/api/test-current.txt b/api/test-current.txt
index 4c277e4..714cac9 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -1234,6 +1234,7 @@
field public static final int summaryOff = 16843248; // 0x10101f0
field public static final int summaryOn = 16843247; // 0x10101ef
field public static final int supportsAssist = 16844016; // 0x10104f0
+ field public static final int supportsDismissingWindow = 16844104; // 0x1010548
field public static final int supportsLaunchVoiceAssistFromKeyguard = 16844017; // 0x10104f1
field public static final int supportsLocalInteraction = 16844047; // 0x101050f
field public static final int supportsPictureInPicture = 16844023; // 0x10104f7
@@ -8984,6 +8985,7 @@
field public static final java.lang.String EXTRA_CHOOSER_TARGETS = "android.intent.extra.CHOOSER_TARGETS";
field public static final java.lang.String EXTRA_CHOSEN_COMPONENT = "android.intent.extra.CHOSEN_COMPONENT";
field public static final java.lang.String EXTRA_CHOSEN_COMPONENT_INTENT_SENDER = "android.intent.extra.CHOSEN_COMPONENT_INTENT_SENDER";
+ field public static final java.lang.String EXTRA_CONTENT_ANNOTATIONS = "android.intent.extra.CONTENT_ANNOTATIONS";
field public static final java.lang.String EXTRA_DATA_REMOVED = "android.intent.extra.DATA_REMOVED";
field public static final java.lang.String EXTRA_DOCK_STATE = "android.intent.extra.DOCK_STATE";
field public static final int EXTRA_DOCK_STATE_CAR = 2; // 0x2
diff --git a/core/java/android/app/admin/DeviceAdminReceiver.java b/core/java/android/app/admin/DeviceAdminReceiver.java
index f2a8777..a248bce 100644
--- a/core/java/android/app/admin/DeviceAdminReceiver.java
+++ b/core/java/android/app/admin/DeviceAdminReceiver.java
@@ -768,6 +768,10 @@
/**
* Called when a new batch of security logs can be retrieved.
*
+ * <p>If a secondary user or profile is created, this callback won't be received until all users
+ * become affiliated again (even if security logging is enabled).
+ * See {@link DevicePolicyManager#setAffiliationIds}
+ *
* <p>This callback is only applicable to device owners.
*
* @param context The running context as per {@link #onReceive}.
@@ -782,13 +786,18 @@
* ever be called when network logging is enabled. The logs can only be retrieved while network
* logging is enabled.
*
+ * <p>If a secondary user or profile is created, this callback won't be received until all users
+ * become affiliated again (even if network logging is enabled). It will also no longer be
+ * possible to retrieve the network logs batch with the most recent {@code batchToken} provided
+ * by this callback. See {@link DevicePolicyManager#setAffiliationIds}.
+ *
* <p>This callback is only applicable to device owners.
*
* @param context The running context as per {@link #onReceive}.
* @param intent The received intent as per {@link #onReceive}.
* @param batchToken The token representing the current batch of network logs.
* @param networkLogsCount The total count of events in the current batch of network logs.
- * @see DevicePolicyManager#retrieveNetworkLogs(ComponentName)
+ * @see DevicePolicyManager#retrieveNetworkLogs
*/
public void onNetworkLogsAvailable(Context context, Intent intent, long batchToken,
int networkLogsCount) {
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index 074326f..c95e011 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -3651,15 +3651,16 @@
/**
* Called by a device owner to request a bugreport.
* <p>
- * There must be only one user on the device, managed by the device owner. Otherwise a
- * {@link SecurityException} will be thrown.
+ * If the device contains secondary users or profiles, they must be affiliated with the device
+ * owner user. Otherwise a {@link SecurityException} will be thrown. See
+ * {@link #setAffiliationIds}.
*
* @param admin Which {@link DeviceAdminReceiver} this request is associated with.
* @return {@code true} if the bugreport collection started successfully, or {@code false} if it
* wasn't triggered because a previous bugreport operation is still active (either the
* bugreport is still running or waiting for the user to share or decline)
- * @throws SecurityException if {@code admin} is not a device owner, or if there are users other
- * than the one managed by the device owner.
+ * @throws SecurityException if {@code admin} is not a device owner, or there is at least one
+ * profile or secondary user that is not affiliated with the device owner user.
*/
public boolean requestBugreport(@NonNull ComponentName admin) {
throwIfParentInstance("requestBugreport");
@@ -6631,14 +6632,16 @@
}
/**
- * Called by device owner to control the security logging feature. Logging can only be
- * enabled on single user devices where the sole user is managed by the device owner.
+ * Called by device owner to control the security logging feature.
*
* <p> Security logs contain various information intended for security auditing purposes.
* See {@link SecurityEvent} for details.
*
- * <p>There must be only one user on the device, managed by the device owner.
- * Otherwise a {@link SecurityException} will be thrown.
+ * <p><strong>Note:</strong> The device owner won't be able to retrieve security logs if there
+ * are unaffiliated secondary users or profiles on the device, regardless of whether the
+ * feature is enabled. Logs will be discarded if the internal buffer fills up while waiting for
+ * all users to become affiliated. Therefore it's recommended that affiliation ids are set for
+ * new users as soon as possible after provisioning via {@link #setAffiliationIds}.
*
* @param admin Which device owner this request is associated with.
* @param enabled whether security logging should be enabled or not.
@@ -6680,13 +6683,16 @@
* <p> Access to the logs is rate limited and it will only return new logs after the device
* owner has been notified via {@link DeviceAdminReceiver#onSecurityLogsAvailable}.
*
- * <p>There must be only one user on the device, managed by the device owner.
- * Otherwise a {@link SecurityException} will be thrown.
+ * <p>If there is any other user or profile on the device, it must be affiliated with the
+ * device owner. Otherwise a {@link SecurityException} will be thrown. See
+ * {@link #setAffiliationIds}
*
* @param admin Which device owner this request is associated with.
* @return the new batch of security logs which is a list of {@link SecurityEvent},
* or {@code null} if rate limitation is exceeded or if logging is currently disabled.
- * @throws SecurityException if {@code admin} is not a device owner.
+ * @throws SecurityException if {@code admin} is not a device owner, or there is at least one
+ * profile or secondary user that is not affiliated with the device owner user.
+ * @see DeviceAdminReceiver#onSecurityLogsAvailable
*/
public @Nullable List<SecurityEvent> retrieveSecurityLogs(@NonNull ComponentName admin) {
throwIfParentInstance("retrieveSecurityLogs");
@@ -6726,14 +6732,17 @@
* will result in {@code null} being returned. The device logs are retrieved from a RAM region
* which is not guaranteed to be corruption-free during power cycles, as a result be cautious
* about data corruption when parsing. </strong>
- * <p>
- * There must be only one user on the device, managed by the device owner. Otherwise a
- * {@link SecurityException} will be thrown.
+ *
+ * <p>If there is any other user or profile on the device, it must be affiliated with the
+ * device owner. Otherwise a {@link SecurityException} will be thrown. See
+ * {@link #setAffiliationIds}
*
* @param admin Which device owner this request is associated with.
* @return Device logs from before the latest reboot of the system, or {@code null} if this API
* is not supported on the device.
- * @throws SecurityException if {@code admin} is not a device owner.
+ * @throws SecurityException if {@code admin} is not a device owner, or there is at least one
+ * profile or secondary user that is not affiliated with the device owner user.
+ * @see #retrieveSecurityLogs
*/
public @Nullable List<SecurityEvent> retrievePreRebootSecurityLogs(
@NonNull ComponentName admin) {
@@ -6939,6 +6948,12 @@
* Indicates the entity that controls the device or profile owner. Two users/profiles are
* affiliated if the set of ids set by their device or profile owners intersect.
*
+ * <p><strong>Note:</strong> Features that depend on user affiliation (such as security logging
+ * or {@link #bindDeviceAdminServiceAsUser}) won't be available when a secondary user or profile
+ * is created, until it becomes affiliated. Therefore it is recommended that the appropriate
+ * affiliation ids are set by its profile owner as soon as possible after the user/profile is
+ * created.
+ *
* @param admin Which profile or device owner this request is associated with.
* @param ids A list of opaque non-empty affiliation ids. Duplicate elements will be ignored.
*
@@ -7138,15 +7153,19 @@
}
/**
- * Called by a device owner to control the network logging feature. Logging can only be
- * enabled on single user devices where the sole user is managed by the device owner. If a new
- * user is added on the device, logging is disabled.
+ * Called by a device owner to control the network logging feature.
*
* <p> Network logs contain DNS lookup and connect() library call events.
*
+ * <p><strong>Note:</strong> The device owner won't be able to retrieve network logs if there
+ * are unaffiliated secondary users or profiles on the device, regardless of whether the
+ * feature is enabled. Logs will be discarded if the internal buffer fills up while waiting for
+ * all users to become affiliated. Therefore it's recommended that affiliation ids are set for
+ * new users as soon as possible after provisioning via {@link #setAffiliationIds}.
+ *
* @param admin Which {@link DeviceAdminReceiver} this request is associated with.
* @param enabled whether network logging should be enabled or not.
- * @throws {@link SecurityException} if {@code admin} is not a device owner.
+ * @throws SecurityException if {@code admin} is not a device owner.
* @see #retrieveNetworkLogs
*/
public void setNetworkLoggingEnabled(@NonNull ComponentName admin, boolean enabled) {
@@ -7164,7 +7183,7 @@
* @param admin Which {@link DeviceAdminReceiver} this request is associated with. Can only
* be {@code null} if the caller has MANAGE_USERS permission.
* @return {@code true} if network logging is enabled by device owner, {@code false} otherwise.
- * @throws {@link SecurityException} if {@code admin} is not a device owner and caller has
+ * @throws SecurityException if {@code admin} is not a device owner and caller has
* no MANAGE_USERS permission
*/
public boolean isNetworkLoggingEnabled(@Nullable ComponentName admin) {
@@ -7190,12 +7209,19 @@
* after the device device owner has been notified via
* {@link DeviceAdminReceiver#onNetworkLogsAvailable}.
*
+ * <p>If a secondary user or profile is created, calling this method will throw a
+ * {@link SecurityException} until all users become affiliated again. It will also no longer be
+ * possible to retrieve the network logs batch with the most recent batchToken provided
+ * by {@link DeviceAdminReceiver#onNetworkLogsAvailable}. See
+ * {@link DevicePolicyManager#setAffiliationIds}.
+ *
* @param admin Which {@link DeviceAdminReceiver} this request is associated with.
* @param batchToken A token of the batch to retrieve
* @return A new batch of network logs which is a list of {@link NetworkEvent}. Returns
* {@code null} if the batch represented by batchToken is no longer available or if
* logging is disabled.
- * @throws {@link SecurityException} if {@code admin} is not a device owner.
+ * @throws SecurityException if {@code admin} is not a device owner, or there is at least one
+ * profile or secondary user that is not affiliated with the device owner user.
* @see DeviceAdminReceiver#onNetworkLogsAvailable
*/
public @Nullable List<NetworkEvent> retrieveNetworkLogs(@NonNull ComponentName admin,
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index b05ceaa..d8358f9 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -3843,6 +3843,52 @@
= "android.intent.extra.CHOOSER_REFINEMENT_INTENT_SENDER";
/**
+ * An {@code ArrayList} of {@code String} annotations describing content for
+ * {@link #ACTION_CHOOSER}.
+ *
+ * <p>If {@link #EXTRA_CONTENT_ANNOTATIONS} is present in an intent used to start a
+ * {@link #ACTION_CHOOSER} activity, the first three annotations will be used to rank apps.</p>
+ *
+ * <p>Annotations should describe the major components or topics of the content. It is up to
+ * apps initiating {@link #ACTION_CHOOSER} to learn and add annotations. Annotations should be
+ * learned in advance, e.g., when creating or saving content, to avoid increasing latency to
+ * start {@link #ACTION_CHOOSER}. Performance on customized annotations can suffer, if they are
+ * rarely used for {@link #ACTION_CHOOSER} in the past 14 days. Therefore, it is recommended to
+ * use the following annotations when applicable:</p>
+ * <ul>
+ * <li>"product": represents that the topic of the content is mainly about products, e.g.,
+ * health & beauty, and office supplies.</li>
+ * <li>"emotion": represents that the topic of the content is mainly about emotions, e.g.,
+ * happy, and sad.</li>
+ * <li>"person": represents that the topic of the content is mainly about persons, e.g.,
+ * face, finger, standing, and walking.</li>
+ * <li>"child": represents that the topic of the content is mainly about children, e.g.,
+ * child, and baby.</li>
+ * <li>"selfie": represents that the topic of the content is mainly about selfies.</li>
+ * <li>"crowd": represents that the topic of the content is mainly about crowds.</li>
+ * <li>"party": represents that the topic of the content is mainly about parties.</li>
+ * <li>"animal": represent that the topic of the content is mainly about animals.</li>
+ * <li>"plant": represents that the topic of the content is mainly about plants, e.g.,
+ * flowers.</li>
+ * <li>"vacation": represents that the topic of the content is mainly about vacations.</li>
+ * <li>"fashion": represents that the topic of the content is mainly about fashion, e.g.
+ * sunglasses, jewelry, handbags and clothing.</li>
+ * <li>"material": represents that the topic of the content is mainly about materials, e.g.,
+ * paper, and silk.</li>
+ * <li>"vehicle": represents that the topic of the content is mainly about vehicles, like
+ * cars, and boats.</li>
+ * <li>"document": represents that the topic of the content is mainly about documents, e.g.
+ * posters.</li>
+ * <li>"design": represents that the topic of the content is mainly about design, e.g. arts
+ * and designs of houses.</li>
+ * <li>"holiday": represents that the topic of the content is mainly about holidays, e.g.,
+ * Christmas and Thanksgiving.</li>
+ * </ul>
+ */
+ public static final String EXTRA_CONTENT_ANNOTATIONS
+ = "android.intent.extra.CONTENT_ANNOTATIONS";
+
+ /**
* A {@link ResultReceiver} used to return data back to the sender.
*
* <p>Used to complete an app-specific
diff --git a/core/java/android/hardware/SensorEvent.java b/core/java/android/hardware/SensorEvent.java
index d67b6a8..9b72757a 100644
--- a/core/java/android/hardware/SensorEvent.java
+++ b/core/java/android/hardware/SensorEvent.java
@@ -438,7 +438,8 @@
* Soft iron - These distortions arise due to the interaction with the earth's magnetic
* field.
* </p>
- * <h4> {@link android.hardware.Sensor#TYPE_GAME_ROTATION_VECTOR}:</h4>
+ * <h4> {@link android.hardware.Sensor#TYPE_GAME_ROTATION_VECTOR
+ * Sensor.TYPE_GAME_ROTATION_VECTOR}:</h4>
* Identical to {@link android.hardware.Sensor#TYPE_ROTATION_VECTOR} except that it
* doesn't use the geomagnetic field. Therefore the Y axis doesn't
* point north, but instead to some other reference, that reference is
@@ -482,22 +483,6 @@
* <p><b>Pro Tip:</b> Always use the length of the values array while performing operations
* on it. In earlier versions, this used to be always 3 which has changed now. </p>
*
- * @see GeomagneticField
- *
- * <h4> {@link android.hardware.Sensor#TYPE_DEVICE_ORIENTATION
- * Sensor.TYPE_DEVICE_ORIENTATION}:</h4>
- * The current device orientation will be available in values[0]. The only
- * available values are:
- * <ul>
- * <li> 0: device is in default orientation (Y axis is vertical and points up)
- * <li> 1: device is rotated 90 degrees counter-clockwise from default
- * orientation (X axis is vertical and points up)
- * <li> 2: device is rotated 180 degrees from default orientation (Y axis is
- * vertical and points down)
- * <li> 3: device is rotated 90 degrees clockwise from default orientation (X axis
- * is vertical and points down)
- * </ul>
- *
* <h4>{@link android.hardware.Sensor#TYPE_POSE_6DOF
* Sensor.TYPE_POSE_6DOF}:</h4>
*
@@ -605,6 +590,8 @@
* temperature compensation is allowed).
* x_bias, y_bias, z_bias are the estimated biases.
* </p>
+ *
+ * @see GeomagneticField
*/
public final float[] values;
diff --git a/core/java/android/os/HwParcel.java b/core/java/android/os/HwParcel.java
index c7612d1..a265dd0 100644
--- a/core/java/android/os/HwParcel.java
+++ b/core/java/android/os/HwParcel.java
@@ -212,7 +212,7 @@
public native final HwBlob readBuffer();
public native final HwBlob readEmbeddedBuffer(
- long parentHandle, long offset);
+ long parentHandle, long offset, boolean nullable);
public native final void writeBuffer(HwBlob blob);
diff --git a/core/java/android/os/INetworkManagementService.aidl b/core/java/android/os/INetworkManagementService.aidl
index 1c3d6bd..dbe2f6d 100644
--- a/core/java/android/os/INetworkManagementService.aidl
+++ b/core/java/android/os/INetworkManagementService.aidl
@@ -312,11 +312,6 @@
*/
void setDnsConfigurationForNetwork(int netId, in String[] servers, String domains);
- /**
- * Bind name servers to a network in the DNS resolver.
- */
- void setDnsServersForNetwork(int netId, in String[] servers, String domains);
-
void setFirewallEnabled(boolean enabled);
boolean isFirewallEnabled();
void setFirewallInterfaceRule(String iface, boolean allow);
diff --git a/core/java/android/os/IRecoverySystem.aidl b/core/java/android/os/IRecoverySystem.aidl
index c5ceecd..1ee83ae 100644
--- a/core/java/android/os/IRecoverySystem.aidl
+++ b/core/java/android/os/IRecoverySystem.aidl
@@ -25,5 +25,5 @@
boolean uncrypt(in String packageFile, IRecoverySystemProgressListener listener);
boolean setupBcb(in String command);
boolean clearBcb();
- void rebootRecoveryWithCommand(in String command);
+ void rebootRecoveryWithCommand(in String command, in boolean update);
}
diff --git a/core/java/android/os/RecoverySystem.java b/core/java/android/os/RecoverySystem.java
index d48431a..7f9ea438 100644
--- a/core/java/android/os/RecoverySystem.java
+++ b/core/java/android/os/RecoverySystem.java
@@ -491,15 +491,10 @@
command += securityArg;
}
+ // RECOVERY_SERVICE writes to BCB (bootloader control block) and triggers the reboot.
RecoverySystem rs = (RecoverySystem) context.getSystemService(
Context.RECOVERY_SERVICE);
- if (!rs.setupBcb(command)) {
- throw new IOException("Setup BCB failed");
- }
-
- // Having set up the BCB (bootloader control block), go ahead and reboot
- PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
- pm.reboot(PowerManager.REBOOT_RECOVERY_UPDATE);
+ rs.rebootRecoveryWithCommand(command, true /* update */);
throw new IOException("Reboot failed (no permissions?)");
}
@@ -713,7 +708,7 @@
// Write the command into BCB (bootloader control block) and boot from
// there. Will not return unless failed.
RecoverySystem rs = (RecoverySystem) context.getSystemService(Context.RECOVERY_SERVICE);
- rs.rebootRecoveryWithCommand(command.toString());
+ rs.rebootRecoveryWithCommand(command.toString(), false);
throw new IOException("Reboot failed (no permissions?)");
}
@@ -913,9 +908,9 @@
* Talks to RecoverySystemService via Binder to set up the BCB command and
* reboot into recovery accordingly.
*/
- private void rebootRecoveryWithCommand(String command) {
+ private void rebootRecoveryWithCommand(String command, boolean update) {
try {
- mService.rebootRecoveryWithCommand(command);
+ mService.rebootRecoveryWithCommand(command, update);
} catch (RemoteException ignored) {
}
}
diff --git a/core/java/android/view/inputmethod/InputMethodInfo.java b/core/java/android/view/inputmethod/InputMethodInfo.java
index 5c8e6dc..b6da1d8 100644
--- a/core/java/android/view/inputmethod/InputMethodInfo.java
+++ b/core/java/android/view/inputmethod/InputMethodInfo.java
@@ -57,6 +57,7 @@
* @attr ref android.R.styleable#InputMethod_settingsActivity
* @attr ref android.R.styleable#InputMethod_isDefault
* @attr ref android.R.styleable#InputMethod_supportsSwitchingToNextInputMethod
+ * @attr ref android.R.styleable#InputMethod_supportsDismissingWindow
*/
public final class InputMethodInfo implements Parcelable {
static final String TAG = "InputMethodInfo";
@@ -104,6 +105,11 @@
private final boolean mSupportsSwitchingToNextInputMethod;
/**
+ * The flag whether this IME supports ways to dismiss its window (e.g. dismiss button.)
+ */
+ private final boolean mSupportsDismissingWindow;
+
+ /**
* Constructor.
*
* @param context The Context in which we are parsing the input method.
@@ -132,6 +138,7 @@
mId = new ComponentName(si.packageName, si.name).flattenToShortString();
boolean isAuxIme = true;
boolean supportsSwitchingToNextInputMethod = false; // false as default
+ boolean supportsDismissingWindow = false; // false as default
mForceDefault = false;
PackageManager pm = context.getPackageManager();
@@ -171,6 +178,8 @@
supportsSwitchingToNextInputMethod = sa.getBoolean(
com.android.internal.R.styleable.InputMethod_supportsSwitchingToNextInputMethod,
false);
+ supportsDismissingWindow = sa.getBoolean(
+ com.android.internal.R.styleable.InputMethod_supportsDismissingWindow, false);
sa.recycle();
final int depth = parser.getDepth();
@@ -242,6 +251,7 @@
mIsDefaultResId = isDefaultResId;
mIsAuxIme = isAuxIme;
mSupportsSwitchingToNextInputMethod = supportsSwitchingToNextInputMethod;
+ mSupportsDismissingWindow = supportsDismissingWindow;
}
InputMethodInfo(Parcel source) {
@@ -250,6 +260,7 @@
mIsDefaultResId = source.readInt();
mIsAuxIme = source.readInt() == 1;
mSupportsSwitchingToNextInputMethod = source.readInt() == 1;
+ mSupportsDismissingWindow = source.readInt() == 1;
mService = ResolveInfo.CREATOR.createFromParcel(source);
mSubtypes = new InputMethodSubtypeArray(source);
mForceDefault = false;
@@ -260,8 +271,10 @@
*/
public InputMethodInfo(String packageName, String className,
CharSequence label, String settingsActivity) {
- this(buildDummyResolveInfo(packageName, className, label), false, settingsActivity, null,
- 0, false /* forceDefault */, true /* supportsSwitchingToNextInputMethod */);
+ this(buildDummyResolveInfo(packageName, className, label), false /* isAuxIme */,
+ settingsActivity, null /* subtypes */, 0 /* isDefaultResId */,
+ false /* forceDefault */, true /* supportsSwitchingToNextInputMethod */,
+ true /* supportsDismissingWindow */);
}
/**
@@ -271,17 +284,18 @@
public InputMethodInfo(ResolveInfo ri, boolean isAuxIme,
String settingsActivity, List<InputMethodSubtype> subtypes, int isDefaultResId,
boolean forceDefault) {
- this(ri, isAuxIme, settingsActivity, subtypes, isDefaultResId,
- forceDefault, true /* supportsSwitchingToNextInputMethod */);
+ this(ri, isAuxIme, settingsActivity, subtypes, isDefaultResId, forceDefault,
+ true /* supportsSwitchingToNextInputMethod */,
+ true /* supportsDismissingWindow */);
}
/**
* Temporary API for creating a built-in input method for test.
* @hide
*/
- public InputMethodInfo(ResolveInfo ri, boolean isAuxIme,
- String settingsActivity, List<InputMethodSubtype> subtypes, int isDefaultResId,
- boolean forceDefault, boolean supportsSwitchingToNextInputMethod) {
+ public InputMethodInfo(ResolveInfo ri, boolean isAuxIme, String settingsActivity,
+ List<InputMethodSubtype> subtypes, int isDefaultResId, boolean forceDefault,
+ boolean supportsSwitchingToNextInputMethod, boolean supportsDismissingWindow) {
final ServiceInfo si = ri.serviceInfo;
mService = ri;
mId = new ComponentName(si.packageName, si.name).flattenToShortString();
@@ -291,6 +305,7 @@
mSubtypes = new InputMethodSubtypeArray(subtypes);
mForceDefault = forceDefault;
mSupportsSwitchingToNextInputMethod = supportsSwitchingToNextInputMethod;
+ mSupportsDismissingWindow = supportsDismissingWindow;
}
private static ResolveInfo buildDummyResolveInfo(String packageName, String className,
@@ -431,7 +446,8 @@
public void dump(Printer pw, String prefix) {
pw.println(prefix + "mId=" + mId
+ " mSettingsActivityName=" + mSettingsActivityName
- + " mSupportsSwitchingToNextInputMethod=" + mSupportsSwitchingToNextInputMethod);
+ + " mSupportsSwitchingToNextInputMethod=" + mSupportsSwitchingToNextInputMethod
+ + " mSupportsDismissingWindow=" + mSupportsDismissingWindow);
pw.println(prefix + "mIsDefaultResId=0x"
+ Integer.toHexString(mIsDefaultResId));
pw.println(prefix + "Service:");
@@ -484,6 +500,14 @@
}
/**
+ * @return true if this input method supports ways to dismiss its window.
+ * @hide
+ */
+ public boolean supportsDismissingWindow() {
+ return mSupportsDismissingWindow;
+ }
+
+ /**
* Used to package this object into a {@link Parcel}.
*
* @param dest The {@link Parcel} to be written.
@@ -496,6 +520,7 @@
dest.writeInt(mIsDefaultResId);
dest.writeInt(mIsAuxIme ? 1 : 0);
dest.writeInt(mSupportsSwitchingToNextInputMethod ? 1 : 0);
+ dest.writeInt(mSupportsDismissingWindow ? 1 : 0);
mService.writeToParcel(dest, flags);
mSubtypes.writeToParcel(dest);
}
diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java
index 7878611..d4baa18 100644
--- a/core/java/com/android/internal/app/ChooserActivity.java
+++ b/core/java/com/android/internal/app/ChooserActivity.java
@@ -418,7 +418,7 @@
}
}
}
- updateChooserCounts(target);
+ updateModelAndChooserCounts(target);
return super.onTargetSelected(target, alwaysCheck);
}
@@ -575,27 +575,18 @@
// Do nothing. We'll send the voice stuff ourselves.
}
- void updateChooserCounts(TargetInfo info) {
+ void updateModelAndChooserCounts(TargetInfo info) {
if (info != null) {
- UsageStatsManager usageStatsManager =
- (UsageStatsManager) getSystemService(Context.USAGE_STATS_SERVICE);
- if (usageStatsManager == null) {
- if (DEBUG) {
- Log.d(TAG, "Can not start UsageStatsManager");
- }
- return;
- }
final ResolveInfo ri = info.getResolveInfo();
Intent targetIntent = getTargetIntent();
if (ri != null && ri.activityInfo != null && targetIntent != null) {
- usageStatsManager.reportChooserSelection(ri.activityInfo.packageName, getUserId(),
- targetIntent.getType(), null, targetIntent.getAction());
if (mAdapter != null) {
mAdapter.updateModel(info.getResolvedComponentName());
+ mAdapter.updateChooserCounts(ri.activityInfo.packageName, getUserId(),
+ targetIntent.getAction());
}
if (DEBUG) {
Log.d(TAG, "ResolveInfo Package is " + ri.activityInfo.packageName);
- Log.d(TAG, "Annotation to be updated is " + targetIntent.getType());
Log.d(TAG, "Action to be updated is " + targetIntent.getAction());
}
} else if(DEBUG) {
@@ -618,7 +609,7 @@
} else {
TargetInfo clonedTarget = selectedTarget.cloneFilledIn(matchingIntent, 0);
if (super.onTargetSelected(clonedTarget, false)) {
- updateChooserCounts(clonedTarget);
+ updateModelAndChooserCounts(clonedTarget);
finish();
return;
}
diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java
index 0950630..d734d17 100644
--- a/core/java/com/android/internal/app/ResolverActivity.java
+++ b/core/java/com/android/internal/app/ResolverActivity.java
@@ -1299,6 +1299,10 @@
mResolverListController.updateModel(componentName);
}
+ public void updateChooserCounts(String packageName, int userId, String action) {
+ mResolverListController.updateChooserCounts(packageName, userId, action);
+ }
+
/**
* Rebuild the list of resolvers. In some cases some parts will need some asynchronous work
* to complete.
diff --git a/core/java/com/android/internal/app/ResolverComparator.java b/core/java/com/android/internal/app/ResolverComparator.java
index 45fad97f..d9ab47e 100644
--- a/core/java/com/android/internal/app/ResolverComparator.java
+++ b/core/java/com/android/internal/app/ResolverComparator.java
@@ -52,6 +52,8 @@
private static final boolean DEBUG = false;
+ private static final int NUM_OF_TOP_ANNOTATIONS_TO_USE = 3;
+
// One week
private static final long USAGE_STATS_PERIOD = 1000 * 60 * 60 * 24 * 7;
@@ -74,7 +76,8 @@
private final long mSinceTime;
private final LinkedHashMap<ComponentName, ScoredTarget> mScoredTargets = new LinkedHashMap<>();
private final String mReferrerPackage;
- public String mContentType;
+ private String mContentType;
+ private String[] mAnnotations;
private String mAction;
private LogisticRegressionAppRanker mRanker;
@@ -91,10 +94,26 @@
mSinceTime = mCurrentTime - USAGE_STATS_PERIOD;
mStats = mUsm.queryAndAggregateUsageStats(mSinceTime, mCurrentTime);
mContentType = intent.getType();
+ getContentAnnotations(intent);
mAction = intent.getAction();
mRanker = new LogisticRegressionAppRanker(context);
}
+ public void getContentAnnotations(Intent intent) {
+ ArrayList<String> annotations = intent.getStringArrayListExtra(
+ Intent.EXTRA_CONTENT_ANNOTATIONS);
+ if (annotations != null) {
+ int size = annotations.size();
+ if (size > NUM_OF_TOP_ANNOTATIONS_TO_USE) {
+ size = NUM_OF_TOP_ANNOTATIONS_TO_USE;
+ }
+ mAnnotations = new String[size];
+ for (int i = 0; i < size; i++) {
+ mAnnotations[i] = annotations.get(i);
+ }
+ }
+ }
+
public void compute(List<ResolvedComponentInfo> targets) {
mScoredTargets.clear();
@@ -132,12 +151,18 @@
if (launched > mostLaunched) {
mostLaunched = launched;
}
- // TODO(kanlig): get and combine counts of categories.
int selected = 0;
if (pkStats.mChooserCounts != null && mAction != null
&& pkStats.mChooserCounts.get(mAction) != null) {
selected = pkStats.mChooserCounts.get(mAction).getOrDefault(mContentType, 0);
+ if (mAnnotations != null) {
+ final int size = mAnnotations.length;
+ for (int i = 0; i < size; i++) {
+ selected += pkStats.mChooserCounts.get(mAction)
+ .getOrDefault(mAnnotations[i], 0);
+ }
+ }
}
if (DEBUG) {
if (mAction == null) {
@@ -288,6 +313,12 @@
}
}
+ public void updateChooserCounts(String packageName, int userId, String action) {
+ if (mUsm != null) {
+ mUsm.reportChooserSelection(packageName, userId, mContentType, mAnnotations, action);
+ }
+ }
+
public void updateModel(ComponentName componentName) {
if (mScoredTargets == null || componentName == null ||
!mScoredTargets.containsKey(componentName)) {
diff --git a/core/java/com/android/internal/app/ResolverListController.java b/core/java/com/android/internal/app/ResolverListController.java
index d864a31..f88f6f9 100644
--- a/core/java/com/android/internal/app/ResolverListController.java
+++ b/core/java/com/android/internal/app/ResolverListController.java
@@ -224,4 +224,10 @@
mResolverComparator.updateModel(componentName);
}
}
+
+ public void updateChooserCounts(String packageName, int userId, String action) {
+ if (mResolverComparator != null) {
+ mResolverComparator.updateChooserCounts(packageName, userId, action);
+ }
+ }
}
diff --git a/core/jni/android_os_HwParcel.cpp b/core/jni/android_os_HwParcel.cpp
index aefdc84..1bd2333 100644
--- a/core/jni/android_os_HwParcel.cpp
+++ b/core/jni/android_os_HwParcel.cpp
@@ -822,7 +822,8 @@
}
static jobject JHwParcel_native_readEmbeddedBuffer(
- JNIEnv *env, jobject thiz, jlong parentHandle, jlong offset) {
+ JNIEnv *env, jobject thiz, jlong parentHandle, jlong offset,
+ jboolean nullable) {
hardware::Parcel *parcel =
JHwParcel::GetNativeContext(env, thiz)->getParcel();
@@ -830,11 +831,15 @@
const void *ptr;
status_t status =
- parcel->readEmbeddedBuffer(&childHandle, parentHandle, offset, &ptr);
+ parcel->readNullableEmbeddedBuffer(&childHandle, parentHandle, offset,
+ &ptr);
if (status != OK) {
jniThrowException(env, "java/util/NoSuchElementException", NULL);
return 0;
+ } else if (status == OK && !nullable && ptr == nullptr) {
+ jniThrowException(env, "java/lang/NullPointerException", NULL);
+ return 0;
}
return JHwBlob::NewObject(env, ptr, childHandle);
@@ -945,7 +950,7 @@
{ "readBuffer", "()L" PACKAGE_PATH "/HwBlob;",
(void *)JHwParcel_native_readBuffer },
- { "readEmbeddedBuffer", "(JJ)L" PACKAGE_PATH "/HwBlob;",
+ { "readEmbeddedBuffer", "(JJZ)L" PACKAGE_PATH "/HwBlob;",
(void *)JHwParcel_native_readEmbeddedBuffer },
{ "writeBuffer", "(L" PACKAGE_PATH "/HwBlob;)V",
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index 5458e7c..dd33718 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -3178,6 +3178,18 @@
and subtype in order to provide the consistent user experience in switching
between IMEs and subtypes. -->
<attr name="supportsSwitchingToNextInputMethod" format="boolean" />
+ <!-- Set to true if this input method supports ways to dismiss the windows assigned to
+ the input method (e.g. a dismiss button rendered by the input method itself). The
+ System UI may optimize the UI by not showing system-level dismiss button if this
+ value is true.
+ <p> Must be a boolean value, either "true" or "false". The default value is "false".
+ <p> This may also be a reference to a resource (in the form "@[package:]type:name")
+ or theme attribute (in the form "?[package:]type:name") containing a value of this
+ type.
+ <p> A UI element that dismisses the input method window should report
+ {@link android.view.accessibility.AccessibilityNodeInfo#ACTION_DISMISS} action, so
+ that accessibility services can handle it accordingly. -->
+ <attr name="supportsDismissingWindow" format="boolean" />
</declare-styleable>
<!-- This is the subtype of InputMethod. Subtype can describe locales (e.g. en_US, fr_FR...)
diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml
index 73cba89..064d31e 100644
--- a/core/res/res/values/public.xml
+++ b/core/res/res/values/public.xml
@@ -2784,6 +2784,7 @@
<public name="focusedByDefault" />
<public name="appCategory" />
<public name="autoSizeMaxTextSize" />
+ <public name="supportsDismissingWindow" />
</public-group>
<public-group type="style" first-id="0x010302e0">
diff --git a/core/tests/coretests/res/xml/ime_meta.xml b/core/tests/coretests/res/xml/ime_meta.xml
new file mode 100644
index 0000000..a975718
--- /dev/null
+++ b/core/tests/coretests/res/xml/ime_meta.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ Copyright (C) 2017 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<input-method
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:settingsActivity="com.android.inputmethod.latin.settings.SettingsActivity"
+>
+ <subtype
+ android:label="subtype1"
+ android:imeSubtypeLocale="en_US"
+ android:imeSubtypeMode="keyboard" />
+</input-method>
diff --git a/core/tests/coretests/res/xml/ime_meta_dismiss.xml b/core/tests/coretests/res/xml/ime_meta_dismiss.xml
new file mode 100644
index 0000000..59f8ecc
--- /dev/null
+++ b/core/tests/coretests/res/xml/ime_meta_dismiss.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ Copyright (C) 2017 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<input-method
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:settingsActivity="com.android.inputmethod.latin.settings.SettingsActivity"
+ android:supportsDismissingWindow="true"
+>
+ <subtype
+ android:label="subtype1"
+ android:imeSubtypeLocale="en_US"
+ android:imeSubtypeMode="keyboard" />
+</input-method>
diff --git a/core/tests/coretests/res/xml/ime_meta_sw_next.xml b/core/tests/coretests/res/xml/ime_meta_sw_next.xml
new file mode 100644
index 0000000..2e2ee33
--- /dev/null
+++ b/core/tests/coretests/res/xml/ime_meta_sw_next.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ Copyright (C) 2017 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<input-method
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:settingsActivity="com.android.inputmethod.latin.settings.SettingsActivity"
+ android:supportsSwitchingToNextInputMethod="true"
+>
+ <subtype
+ android:label="subtype1"
+ android:imeSubtypeLocale="en_US"
+ android:imeSubtypeMode="keyboard" />
+</input-method>
diff --git a/core/tests/coretests/src/android/view/inputmethod/InputMethodInfoTest.java b/core/tests/coretests/src/android/view/inputmethod/InputMethodInfoTest.java
new file mode 100644
index 0000000..23dc80f
--- /dev/null
+++ b/core/tests/coretests/src/android/view/inputmethod/InputMethodInfoTest.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.inputmethod;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+
+import android.annotation.XmlRes;
+import android.content.Context;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.android.frameworks.coretests.R;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class InputMethodInfoTest {
+
+ @Test
+ public void testEqualsAndHashCode() throws Exception {
+ final InputMethodInfo imi = buildInputMethodForTest(R.xml.ime_meta);
+ final InputMethodInfo clone = cloneViaParcel(imi);
+
+ assertThat(clone.equals(imi), is(true));
+ assertThat(clone.hashCode(), equalTo(imi.hashCode()));
+ }
+
+ @Test
+ public void testBooleanAttributes_DefaultValues() throws Exception {
+ final InputMethodInfo imi = buildInputMethodForTest(R.xml.ime_meta);
+
+ assertThat(imi.supportsSwitchingToNextInputMethod(), is(false));
+ assertThat(imi.supportsDismissingWindow(), is(false));
+
+ final InputMethodInfo clone = cloneViaParcel(imi);
+
+ assertThat(clone.supportsSwitchingToNextInputMethod(), is(false));
+ assertThat(clone.supportsDismissingWindow(), is(false));
+ }
+
+ @Test
+ public void testSupportsSwitchingToNextInputMethod() throws Exception {
+ final InputMethodInfo imi = buildInputMethodForTest(R.xml.ime_meta_sw_next);
+
+ assertThat(imi.supportsSwitchingToNextInputMethod(), is(true));
+
+ final InputMethodInfo clone = cloneViaParcel(imi);
+
+ assertThat(clone.supportsSwitchingToNextInputMethod(), is(true));
+ }
+
+ @Test
+ public void testSupportsDismissingWindow() throws Exception {
+ final InputMethodInfo imi = buildInputMethodForTest(R.xml.ime_meta_dismiss);
+
+ assertThat(imi.supportsDismissingWindow(), is(true));
+
+ final InputMethodInfo clone = cloneViaParcel(imi);
+
+ assertThat(clone.supportsDismissingWindow(), is(true));
+ }
+
+ private InputMethodInfo buildInputMethodForTest(final @XmlRes int metaDataRes)
+ throws Exception {
+ final Context context = InstrumentationRegistry.getContext();
+ final ServiceInfo serviceInfo = new ServiceInfo();
+ serviceInfo.applicationInfo = context.getApplicationInfo();
+ serviceInfo.packageName = context.getPackageName();
+ serviceInfo.name = "DummyImeForTest";
+ serviceInfo.metaData = new Bundle();
+ serviceInfo.metaData.putInt(InputMethod.SERVICE_META_DATA, metaDataRes);
+ final ResolveInfo resolveInfo = new ResolveInfo();
+ resolveInfo.serviceInfo = serviceInfo;
+ return new InputMethodInfo(context, resolveInfo, null /* additionalSubtypesMap */);
+ }
+
+ private InputMethodInfo cloneViaParcel(final InputMethodInfo original) {
+ Parcel parcel = null;
+ try {
+ parcel = Parcel.obtain();
+ original.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ return InputMethodInfo.CREATOR.createFromParcel(parcel);
+ } finally {
+ if (parcel != null) {
+ parcel.recycle();
+ }
+ }
+ }
+}
diff --git a/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java b/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java
index 8385b69..aab4698 100644
--- a/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java
+++ b/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java
@@ -124,7 +124,7 @@
}
@Test
- public void updateChooserCountsAfterUserSelection() throws InterruptedException {
+ public void updateChooserCountsAndModelAfterUserSelection() throws InterruptedException {
Intent sendIntent = createSendImageIntent();
List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
@@ -142,19 +142,15 @@
sOverrides.onSafelyStartCallback = targetInfo -> {
return true;
};
- String action = sendIntent.getAction();
- String annotation = sendIntent.getType();
ResolveInfo toChoose = resolvedComponentInfos.get(0).getResolveInfoAt(0);
- String packageName = toChoose.activityInfo.packageName;
- long toChooseCount = getCount(usm, packageName, action, annotation);
onView(withText(toChoose.activityInfo.name))
.perform(click());
waitForIdle();
verify(sOverrides.resolverListController, times(1))
+ .updateChooserCounts(Mockito.anyString(), Mockito.anyInt(), Mockito.anyString());
+ verify(sOverrides.resolverListController, times(1))
.updateModel(toChoose.activityInfo.getComponentName());
assertThat(activity.getIsSelected(), is(true));
- long updatedCount = getCount(usm, packageName, action, annotation);
- assertThat(updatedCount, is(toChooseCount + 1l));
}
@Test
diff --git a/core/tests/coretests/src/com/android/internal/inputmethod/InputMethodSubtypeSwitchingControllerTest.java b/core/tests/coretests/src/com/android/internal/inputmethod/InputMethodSubtypeSwitchingControllerTest.java
index ba5206a..34c34d7 100644
--- a/core/tests/coretests/src/com/android/internal/inputmethod/InputMethodSubtypeSwitchingControllerTest.java
+++ b/core/tests/coretests/src/com/android/internal/inputmethod/InputMethodSubtypeSwitchingControllerTest.java
@@ -74,7 +74,8 @@
}
final InputMethodInfo imi = new InputMethodInfo(ri, DUMMY_IS_AUX_IME,
DUMMY_SETTING_ACTIVITY_NAME, subtypes, DUMMY_IS_DEFAULT_RES_ID,
- DUMMY_FORCE_DEFAULT, supportsSwitchingToNextInputMethod);
+ DUMMY_FORCE_DEFAULT, supportsSwitchingToNextInputMethod,
+ false /* supportsDismissingWindow */);
if (subtypes == null) {
items.add(new ImeSubtypeListItem(imeName, null /* variableName */, imi,
NOT_A_SUBTYPE_ID, null, SYSTEM_LOCALE));
diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/users/AppRestrictionsHelperTest.java b/packages/SettingsLib/tests/integ/src/com/android/settingslib/users/AppRestrictionsHelperTest.java
index f33362f8..4df199c 100644
--- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/users/AppRestrictionsHelperTest.java
+++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/users/AppRestrictionsHelperTest.java
@@ -159,14 +159,14 @@
for (String pkg : defaultImes) {
final ResolveInfo ri = createResolveInfoForSystemApp(pkg);
final InputMethodInfo inputMethodInfo = new InputMethodInfo(
- ri, false, null, null, 0, true, true);
+ ri, false, null, null, 0, true, true, false);
inputMethods.add(inputMethodInfo);
addInstalledApp(ri);
}
for (String pkg : otherImes) {
final ResolveInfo ri = createResolveInfoForSystemApp(pkg);
final InputMethodInfo inputMethodInfo = new InputMethodInfo(
- ri, false, null, null, 0, false, true);
+ ri, false, null, null, 0, false, true, false);
inputMethods.add(inputMethodInfo);
addInstalledApp(ri);
}
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 05963ac..69515fa 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -460,7 +460,7 @@
<string name="accessibility_desc_settings">Settings</string>
<!-- Content description for the recent apps panel (not shown on the screen). [CHAR LIMIT=NONE] -->
<string name="accessibility_desc_recent_apps">Overview.</string>
- <!-- Content description for the graphic shown instead of an activity window while the activity is locked (not shown on the screen). [CHAR LIMIT=NONE] -->
+ <!-- Content description for the work profile lock screen. This prevents work profile apps from being used, but personal apps can be used as normal (not shown on the screen). [CHAR LIMIT=NONE] -->
<string name="accessibility_desc_work_lock">Work lock screen</string>
<!-- Content description for the close button in the zen mode panel introduction message. [CHAR LIMIT=NONE] -->
<string name="accessibility_desc_close">Close</string>
diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java
index f271b08..0a088e9 100644
--- a/services/core/java/com/android/server/ConnectivityService.java
+++ b/services/core/java/com/android/server/ConnectivityService.java
@@ -883,7 +883,7 @@
private void handleMobileDataAlwaysOn() {
final boolean enable = (Settings.Global.getInt(
- mContext.getContentResolver(), Settings.Global.MOBILE_DATA_ALWAYS_ON, 0) == 1);
+ mContext.getContentResolver(), Settings.Global.MOBILE_DATA_ALWAYS_ON, 1) == 1);
final boolean isEnabled = (mNetworkRequests.get(mDefaultMobileDataRequest) != null);
if (enable == isEnabled) {
return; // Nothing to do.
diff --git a/services/core/java/com/android/server/NetworkManagementService.java b/services/core/java/com/android/server/NetworkManagementService.java
index 5654096..74e44d5 100644
--- a/services/core/java/com/android/server/NetworkManagementService.java
+++ b/services/core/java/com/android/server/NetworkManagementService.java
@@ -1912,31 +1912,6 @@
}
@Override
- public void setDnsServersForNetwork(int netId, String[] servers, String domains) {
- mContext.enforceCallingOrSelfPermission(CONNECTIVITY_INTERNAL, TAG);
-
- Command cmd;
- if (servers.length > 0) {
- cmd = new Command("resolver", "setnetdns", netId,
- (domains == null ? "" : domains));
- for (String s : servers) {
- InetAddress a = NetworkUtils.numericToInetAddress(s);
- if (a.isAnyLocalAddress() == false) {
- cmd.appendArg(a.getHostAddress());
- }
- }
- } else {
- cmd = new Command("resolver", "clearnetdns", netId);
- }
-
- try {
- mConnector.execute(cmd);
- } catch (NativeDaemonConnectorException e) {
- throw e.rethrowAsParcelableException();
- }
- }
-
- @Override
public void addVpnUidRanges(int netId, UidRange[] ranges) {
mContext.enforceCallingOrSelfPermission(CONNECTIVITY_INTERNAL, TAG);
Object[] argv = new Object[3 + MAX_UID_RANGES_PER_COMMAND];
diff --git a/services/core/java/com/android/server/RecoverySystemService.java b/services/core/java/com/android/server/RecoverySystemService.java
index 3c8c699..2010e64 100644
--- a/services/core/java/com/android/server/RecoverySystemService.java
+++ b/services/core/java/com/android/server/RecoverySystemService.java
@@ -181,7 +181,7 @@
}
@Override // Binder call
- public void rebootRecoveryWithCommand(String command) {
+ public void rebootRecoveryWithCommand(String command, boolean update) {
if (DEBUG) Slog.d(TAG, "rebootRecoveryWithCommand: [" + command + "]");
synchronized (sRequestLock) {
if (!setupOrClearBcb(true, command)) {
@@ -190,7 +190,10 @@
// Having set up the BCB, go ahead and reboot.
PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
- pm.reboot(PowerManager.REBOOT_RECOVERY);
+ // PowerManagerService may additionally request uncrypting the package when it's
+ // to install an update (REBOOT_RECOVERY_UPDATE).
+ pm.reboot(update ? PowerManager.REBOOT_RECOVERY_UPDATE :
+ PowerManager.REBOOT_RECOVERY);
}
}
diff --git a/services/core/java/com/android/server/connectivity/Tethering.java b/services/core/java/com/android/server/connectivity/Tethering.java
index 57381f2..0c80166 100644
--- a/services/core/java/com/android/server/connectivity/Tethering.java
+++ b/services/core/java/com/android/server/connectivity/Tethering.java
@@ -32,7 +32,6 @@
import android.content.res.Resources;
import android.hardware.usb.UsbManager;
import android.net.ConnectivityManager;
-import android.net.ConnectivityManager.NetworkCallback;
import android.net.INetworkPolicyManager;
import android.net.INetworkStatsService;
import android.net.LinkProperties;
@@ -73,6 +72,7 @@
import com.android.server.connectivity.tethering.IPv6TetheringCoordinator;
import com.android.server.connectivity.tethering.IPv6TetheringInterfaceServices;
import com.android.server.connectivity.tethering.TetherInterfaceStateMachine;
+import com.android.server.connectivity.tethering.UpstreamNetworkMonitor;
import com.android.server.net.BaseNetworkObserver;
import java.io.FileDescriptor;
@@ -1031,249 +1031,6 @@
}
}
- /**
- * A class to centralize all the network and link properties information
- * pertaining to the current and any potential upstream network.
- *
- * Calling #start() registers two callbacks: one to track the system default
- * network and a second to specifically observe TYPE_MOBILE_DUN networks.
- *
- * The methods and data members of this class are only to be accessed and
- * modified from the tethering master state machine thread. Any other
- * access semantics would necessitate the addition of locking.
- *
- * TODO: Investigate whether more "upstream-specific" logic/functionality
- * could/should be moved here.
- */
- public class UpstreamNetworkMonitor {
- public static final int EVENT_ON_AVAILABLE = 1;
- public static final int EVENT_ON_CAPABILITIES = 2;
- public static final int EVENT_ON_LINKPROPERTIES = 3;
- public static final int EVENT_ON_LOST = 4;
-
- private final Context mContext;
- private final StateMachine mTarget;
- private final int mWhat;
- private final HashMap<Network, NetworkState> mNetworkMap = new HashMap<>();
- private ConnectivityManager mCM;
- private NetworkCallback mDefaultNetworkCallback;
- private NetworkCallback mDunTetheringCallback;
- private NetworkCallback mMobileNetworkCallback;
- private boolean mDunRequired;
-
- public UpstreamNetworkMonitor(Context ctx, StateMachine tgt, int what) {
- mContext = ctx;
- mTarget = tgt;
- mWhat = what;
- }
-
- public void start() {
- stop();
-
- mDefaultNetworkCallback = new UpstreamNetworkCallback();
- cm().registerDefaultNetworkCallback(mDefaultNetworkCallback);
-
- final NetworkRequest dunTetheringRequest = new NetworkRequest.Builder()
- .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
- .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
- .addCapability(NetworkCapabilities.NET_CAPABILITY_DUN)
- .build();
- mDunTetheringCallback = new UpstreamNetworkCallback();
- cm().registerNetworkCallback(dunTetheringRequest, mDunTetheringCallback);
- }
-
- public void stop() {
- releaseMobileNetworkRequest();
-
- releaseCallback(mDefaultNetworkCallback);
- mDefaultNetworkCallback = null;
-
- releaseCallback(mDunTetheringCallback);
- mDunTetheringCallback = null;
-
- mNetworkMap.clear();
- }
-
- public void mobileUpstreamRequiresDun(boolean dunRequired) {
- final boolean valueChanged = (mDunRequired != dunRequired);
- mDunRequired = dunRequired;
- if (valueChanged && mobileNetworkRequested()) {
- releaseMobileNetworkRequest();
- registerMobileNetworkRequest();
- }
- }
-
- public boolean mobileNetworkRequested() {
- return (mMobileNetworkCallback != null);
- }
-
- public void registerMobileNetworkRequest() {
- if (mMobileNetworkCallback != null) return;
-
- final NetworkRequest.Builder builder = new NetworkRequest.Builder()
- .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR);
- if (mDunRequired) {
- builder.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
- .addCapability(NetworkCapabilities.NET_CAPABILITY_DUN);
- } else {
- builder.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
- }
- final NetworkRequest mobileUpstreamRequest = builder.build();
-
- // The existing default network and DUN callbacks will be notified.
- // Therefore, to avoid duplicate notifications, we only register a no-op.
- mMobileNetworkCallback = new NetworkCallback();
-
- // TODO: Change the timeout from 0 (no onUnavailable callback) to use some
- // moderate callback time (once timeout callbacks are implemented). This might
- // be useful for updating some UI. Additionally, we should definitely log a
- // message to aid in any subsequent debugging
- if (DBG) Log.d(TAG, "requesting mobile upstream network: " + mobileUpstreamRequest);
-
- cm().requestNetwork(mobileUpstreamRequest, mMobileNetworkCallback);
- }
-
- public void releaseMobileNetworkRequest() {
- if (mMobileNetworkCallback == null) return;
-
- cm().unregisterNetworkCallback(mMobileNetworkCallback);
- mMobileNetworkCallback = null;
- }
-
- public NetworkState lookup(Network network) {
- return (network != null) ? mNetworkMap.get(network) : null;
- }
-
- private void handleAvailable(Network network) {
- if (VDBG) {
- Log.d(TAG, "EVENT_ON_AVAILABLE for " + network);
- }
- if (!mNetworkMap.containsKey(network)) {
- mNetworkMap.put(network,
- new NetworkState(null, null, null, network, null, null));
- }
-
- final ConnectivityManager cm = cm();
-
- if (mDefaultNetworkCallback != null) {
- cm.requestNetworkCapabilities(mDefaultNetworkCallback);
- cm.requestLinkProperties(mDefaultNetworkCallback);
- }
-
- // Requesting updates for mDunTetheringCallback is not
- // necessary. Because it's a listen, it will already have
- // heard all NetworkCapabilities and LinkProperties updates
- // since UpstreamNetworkMonitor was started. Because we
- // start UpstreamNetworkMonitor before chooseUpstreamType()
- // is ever invoked (it can register a DUN request) this is
- // mostly safe. However, if a DUN network is already up for
- // some reason (unlikely, because DUN is restricted and,
- // unless the DUN network is shared with another APN, only
- // the system can request it and this is the only part of
- // the system that requests it) we won't know its
- // LinkProperties or NetworkCapabilities.
-
- notifyTarget(EVENT_ON_AVAILABLE, network);
- }
-
- private void handleNetCap(Network network, NetworkCapabilities newNc) {
- if (!mNetworkMap.containsKey(network)) {
- // Ignore updates for networks for which we have not yet
- // received onAvailable() - which should never happen -
- // or for which we have already received onLost().
- return;
- }
- if (VDBG) {
- Log.d(TAG, String.format("EVENT_ON_CAPABILITIES for %s: %s",
- network, newNc));
- }
-
- final NetworkState prev = mNetworkMap.get(network);
- mNetworkMap.put(network,
- new NetworkState(null, prev.linkProperties, newNc,
- network, null, null));
- notifyTarget(EVENT_ON_CAPABILITIES, network);
- }
-
- private void handleLinkProp(Network network, LinkProperties newLp) {
- if (!mNetworkMap.containsKey(network)) {
- // Ignore updates for networks for which we have not yet
- // received onAvailable() - which should never happen -
- // or for which we have already received onLost().
- return;
- }
- if (VDBG) {
- Log.d(TAG, String.format("EVENT_ON_LINKPROPERTIES for %s: %s",
- network, newLp));
- }
-
- final NetworkState prev = mNetworkMap.get(network);
- mNetworkMap.put(network,
- new NetworkState(null, newLp, prev.networkCapabilities,
- network, null, null));
- notifyTarget(EVENT_ON_LINKPROPERTIES, network);
- }
-
- private void handleLost(Network network) {
- if (!mNetworkMap.containsKey(network)) {
- // Ignore updates for networks for which we have not yet
- // received onAvailable() - which should never happen -
- // or for which we have already received onLost().
- return;
- }
- if (VDBG) {
- Log.d(TAG, "EVENT_ON_LOST for " + network);
- }
- notifyTarget(EVENT_ON_LOST, mNetworkMap.remove(network));
- }
-
- // Fetch (and cache) a ConnectivityManager only if and when we need one.
- private ConnectivityManager cm() {
- if (mCM == null) {
- mCM = mContext.getSystemService(ConnectivityManager.class);
- }
- return mCM;
- }
-
- /**
- * A NetworkCallback class that relays information of interest to the
- * tethering master state machine thread for subsequent processing.
- */
- private class UpstreamNetworkCallback extends NetworkCallback {
- @Override
- public void onAvailable(Network network) {
- mTarget.getHandler().post(() -> handleAvailable(network));
- }
-
- @Override
- public void onCapabilitiesChanged(Network network, NetworkCapabilities newNc) {
- mTarget.getHandler().post(() -> handleNetCap(network, newNc));
- }
-
- @Override
- public void onLinkPropertiesChanged(Network network, LinkProperties newLp) {
- mTarget.getHandler().post(() -> handleLinkProp(network, newLp));
- }
-
- @Override
- public void onLost(Network network) {
- mTarget.getHandler().post(() -> handleLost(network));
- }
- }
-
- private void releaseCallback(NetworkCallback cb) {
- if (cb != null) cm().unregisterNetworkCallback(cb);
- }
-
- private void notifyTarget(int which, Network network) {
- notifyTarget(which, mNetworkMap.get(network));
- }
-
- private void notifyTarget(int which, NetworkState netstate) {
- mTarget.sendMessage(mWhat, which, 0, netstate);
- }
- }
-
// Needed because the canonical source of upstream truth is just the
// upstream interface name, |mCurrentUpstreamIface|. This is ripe for
// future simplification, once the upstream Network is canonical.
diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java
index afc6247..a5876dd 100644
--- a/services/core/java/com/android/server/connectivity/Vpn.java
+++ b/services/core/java/com/android/server/connectivity/Vpn.java
@@ -1586,9 +1586,6 @@
public void exit() {
// We assume that everything is reset after stopping the daemons.
interrupt();
- for (LocalSocket socket : mSockets) {
- IoUtils.closeQuietly(socket);
- }
agentDisconnect();
try {
mContext.unregisterReceiver(mBroadcastReceiver);
@@ -1601,8 +1598,26 @@
Log.v(TAG, "Waiting");
synchronized (TAG) {
Log.v(TAG, "Executing");
- execute();
- monitorDaemons();
+ try {
+ execute();
+ monitorDaemons();
+ interrupted(); // Clear interrupt flag if execute called exit.
+ } catch (InterruptedException e) {
+ } finally {
+ for (LocalSocket socket : mSockets) {
+ IoUtils.closeQuietly(socket);
+ }
+ // This sleep is necessary for racoon to successfully complete sending delete
+ // message to server.
+ try {
+ Thread.sleep(50);
+ } catch (InterruptedException e) {
+ }
+ for (String daemon : mDaemons) {
+ SystemService.stop(daemon);
+ }
+ }
+ agentDisconnect();
}
}
@@ -1801,18 +1816,6 @@
Log.i(TAG, "Aborting", e);
updateState(DetailedState.FAILED, e.getMessage());
exit();
- } finally {
- // Kill the daemons if they fail to stop.
- if (!initFinished) {
- for (String daemon : mDaemons) {
- SystemService.stop(daemon);
- }
- }
-
- // Do not leave an unstable state.
- if (!initFinished || mNetworkInfo.getDetailedState() == DetailedState.CONNECTING) {
- agentDisconnect();
- }
}
}
@@ -1820,28 +1823,17 @@
* Monitor the daemons we started, moving to disconnected state if the
* underlying services fail.
*/
- private void monitorDaemons() {
+ private void monitorDaemons() throws InterruptedException{
if (!mNetworkInfo.isConnected()) {
return;
}
-
- try {
- while (true) {
- Thread.sleep(2000);
- for (int i = 0; i < mDaemons.length; i++) {
- if (mArguments[i] != null && SystemService.isStopped(mDaemons[i])) {
- return;
- }
+ while (true) {
+ Thread.sleep(2000);
+ for (int i = 0; i < mDaemons.length; i++) {
+ if (mArguments[i] != null && SystemService.isStopped(mDaemons[i])) {
+ return;
}
}
- } catch (InterruptedException e) {
- Log.d(TAG, "interrupted during monitorDaemons(); stopping services");
- } finally {
- for (String daemon : mDaemons) {
- SystemService.stop(daemon);
- }
-
- agentDisconnect();
}
}
}
diff --git a/services/core/java/com/android/server/connectivity/tethering/UpstreamNetworkMonitor.java b/services/core/java/com/android/server/connectivity/tethering/UpstreamNetworkMonitor.java
new file mode 100644
index 0000000..927dfd5
--- /dev/null
+++ b/services/core/java/com/android/server/connectivity/tethering/UpstreamNetworkMonitor.java
@@ -0,0 +1,287 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.tethering;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.ConnectivityManager.NetworkCallback;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.net.NetworkState;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.StateMachine;
+
+import java.util.HashMap;
+
+
+/**
+ * A class to centralize all the network and link properties information
+ * pertaining to the current and any potential upstream network.
+ *
+ * Calling #start() registers two callbacks: one to track the system default
+ * network and a second to specifically observe TYPE_MOBILE_DUN networks.
+ *
+ * The methods and data members of this class are only to be accessed and
+ * modified from the tethering master state machine thread. Any other
+ * access semantics would necessitate the addition of locking.
+ *
+ * TODO: Move upstream selection logic here.
+ *
+ * @hide
+ */
+public class UpstreamNetworkMonitor {
+ private static final String TAG = UpstreamNetworkMonitor.class.getSimpleName();
+ private static final boolean DBG = false;
+ private static final boolean VDBG = false;
+
+ public static final int EVENT_ON_AVAILABLE = 1;
+ public static final int EVENT_ON_CAPABILITIES = 2;
+ public static final int EVENT_ON_LINKPROPERTIES = 3;
+ public static final int EVENT_ON_LOST = 4;
+
+ private final Context mContext;
+ private final StateMachine mTarget;
+ private final int mWhat;
+ private final HashMap<Network, NetworkState> mNetworkMap = new HashMap<>();
+ private ConnectivityManager mCM;
+ private NetworkCallback mDefaultNetworkCallback;
+ private NetworkCallback mDunTetheringCallback;
+ private NetworkCallback mMobileNetworkCallback;
+ private boolean mDunRequired;
+
+ public UpstreamNetworkMonitor(Context ctx, StateMachine tgt, int what) {
+ mContext = ctx;
+ mTarget = tgt;
+ mWhat = what;
+ }
+
+ @VisibleForTesting
+ public UpstreamNetworkMonitor(StateMachine tgt, int what, ConnectivityManager cm) {
+ this(null, tgt, what);
+ mCM = cm;
+ }
+
+ public void start() {
+ stop();
+
+ mDefaultNetworkCallback = new UpstreamNetworkCallback();
+ cm().registerDefaultNetworkCallback(mDefaultNetworkCallback);
+
+ final NetworkRequest dunTetheringRequest = new NetworkRequest.Builder()
+ .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
+ .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_DUN)
+ .build();
+ mDunTetheringCallback = new UpstreamNetworkCallback();
+ cm().registerNetworkCallback(dunTetheringRequest, mDunTetheringCallback);
+ }
+
+ public void stop() {
+ releaseMobileNetworkRequest();
+
+ releaseCallback(mDefaultNetworkCallback);
+ mDefaultNetworkCallback = null;
+
+ releaseCallback(mDunTetheringCallback);
+ mDunTetheringCallback = null;
+
+ mNetworkMap.clear();
+ }
+
+ public void mobileUpstreamRequiresDun(boolean dunRequired) {
+ final boolean valueChanged = (mDunRequired != dunRequired);
+ mDunRequired = dunRequired;
+ if (valueChanged && mobileNetworkRequested()) {
+ releaseMobileNetworkRequest();
+ registerMobileNetworkRequest();
+ }
+ }
+
+ public boolean mobileNetworkRequested() {
+ return (mMobileNetworkCallback != null);
+ }
+
+ public void registerMobileNetworkRequest() {
+ if (mMobileNetworkCallback != null) return;
+
+ final NetworkRequest.Builder builder = new NetworkRequest.Builder()
+ .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR);
+ if (mDunRequired) {
+ builder.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_DUN);
+ } else {
+ builder.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
+ }
+ final NetworkRequest mobileUpstreamRequest = builder.build();
+
+ // The existing default network and DUN callbacks will be notified.
+ // Therefore, to avoid duplicate notifications, we only register a no-op.
+ mMobileNetworkCallback = new NetworkCallback();
+
+ // TODO: Change the timeout from 0 (no onUnavailable callback) to use some
+ // moderate callback time (once timeout callbacks are implemented). This might
+ // be useful for updating some UI. Additionally, we should definitely log a
+ // message to aid in any subsequent debugging
+ if (DBG) Log.d(TAG, "requesting mobile upstream network: " + mobileUpstreamRequest);
+
+ cm().requestNetwork(mobileUpstreamRequest, mMobileNetworkCallback);
+ }
+
+ public void releaseMobileNetworkRequest() {
+ if (mMobileNetworkCallback == null) return;
+
+ cm().unregisterNetworkCallback(mMobileNetworkCallback);
+ mMobileNetworkCallback = null;
+ }
+
+ public NetworkState lookup(Network network) {
+ return (network != null) ? mNetworkMap.get(network) : null;
+ }
+
+ private void handleAvailable(Network network) {
+ if (VDBG) {
+ Log.d(TAG, "EVENT_ON_AVAILABLE for " + network);
+ }
+ if (!mNetworkMap.containsKey(network)) {
+ mNetworkMap.put(network,
+ new NetworkState(null, null, null, network, null, null));
+ }
+
+ final ConnectivityManager cm = cm();
+
+ if (mDefaultNetworkCallback != null) {
+ cm.requestNetworkCapabilities(mDefaultNetworkCallback);
+ cm.requestLinkProperties(mDefaultNetworkCallback);
+ }
+
+ // Requesting updates for mDunTetheringCallback is not
+ // necessary. Because it's a listen, it will already have
+ // heard all NetworkCapabilities and LinkProperties updates
+ // since UpstreamNetworkMonitor was started. Because we
+ // start UpstreamNetworkMonitor before chooseUpstreamType()
+ // is ever invoked (it can register a DUN request) this is
+ // mostly safe. However, if a DUN network is already up for
+ // some reason (unlikely, because DUN is restricted and,
+ // unless the DUN network is shared with another APN, only
+ // the system can request it and this is the only part of
+ // the system that requests it) we won't know its
+ // LinkProperties or NetworkCapabilities.
+
+ notifyTarget(EVENT_ON_AVAILABLE, network);
+ }
+
+ private void handleNetCap(Network network, NetworkCapabilities newNc) {
+ if (!mNetworkMap.containsKey(network)) {
+ // Ignore updates for networks for which we have not yet
+ // received onAvailable() - which should never happen -
+ // or for which we have already received onLost().
+ return;
+ }
+ if (VDBG) {
+ Log.d(TAG, String.format("EVENT_ON_CAPABILITIES for %s: %s",
+ network, newNc));
+ }
+
+ final NetworkState prev = mNetworkMap.get(network);
+ mNetworkMap.put(network,
+ new NetworkState(null, prev.linkProperties, newNc,
+ network, null, null));
+ notifyTarget(EVENT_ON_CAPABILITIES, network);
+ }
+
+ private void handleLinkProp(Network network, LinkProperties newLp) {
+ if (!mNetworkMap.containsKey(network)) {
+ // Ignore updates for networks for which we have not yet
+ // received onAvailable() - which should never happen -
+ // or for which we have already received onLost().
+ return;
+ }
+ if (VDBG) {
+ Log.d(TAG, String.format("EVENT_ON_LINKPROPERTIES for %s: %s",
+ network, newLp));
+ }
+
+ final NetworkState prev = mNetworkMap.get(network);
+ mNetworkMap.put(network,
+ new NetworkState(null, newLp, prev.networkCapabilities,
+ network, null, null));
+ notifyTarget(EVENT_ON_LINKPROPERTIES, network);
+ }
+
+ private void handleLost(Network network) {
+ if (!mNetworkMap.containsKey(network)) {
+ // Ignore updates for networks for which we have not yet
+ // received onAvailable() - which should never happen -
+ // or for which we have already received onLost().
+ return;
+ }
+ if (VDBG) {
+ Log.d(TAG, "EVENT_ON_LOST for " + network);
+ }
+ notifyTarget(EVENT_ON_LOST, mNetworkMap.remove(network));
+ }
+
+ // Fetch (and cache) a ConnectivityManager only if and when we need one.
+ private ConnectivityManager cm() {
+ if (mCM == null) {
+ mCM = mContext.getSystemService(ConnectivityManager.class);
+ }
+ return mCM;
+ }
+
+ /**
+ * A NetworkCallback class that relays information of interest to the
+ * tethering master state machine thread for subsequent processing.
+ */
+ private class UpstreamNetworkCallback extends NetworkCallback {
+ @Override
+ public void onAvailable(Network network) {
+ mTarget.getHandler().post(() -> handleAvailable(network));
+ }
+
+ @Override
+ public void onCapabilitiesChanged(Network network, NetworkCapabilities newNc) {
+ mTarget.getHandler().post(() -> handleNetCap(network, newNc));
+ }
+
+ @Override
+ public void onLinkPropertiesChanged(Network network, LinkProperties newLp) {
+ mTarget.getHandler().post(() -> handleLinkProp(network, newLp));
+ }
+
+ @Override
+ public void onLost(Network network) {
+ mTarget.getHandler().post(() -> handleLost(network));
+ }
+ }
+
+ private void releaseCallback(NetworkCallback cb) {
+ if (cb != null) cm().unregisterNetworkCallback(cb);
+ }
+
+ private void notifyTarget(int which, Network network) {
+ notifyTarget(which, mNetworkMap.get(network));
+ }
+
+ private void notifyTarget(int which, NetworkState netstate) {
+ mTarget.sendMessage(mWhat, which, 0, netstate);
+ }
+}
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
index 7c5550a..d81e092 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
@@ -796,7 +796,7 @@
void onNewAvrAdded(HdmiDeviceInfo avr) {
assertRunOnServiceThread();
addAndStartAction(new SystemAudioAutoInitiationAction(this, avr.getLogicalAddress()));
- if (isArcFeatureEnabled(avr.getPortId())
+ if (isConnected(avr.getPortId()) && isArcFeatureEnabled(avr.getPortId())
&& !hasAction(SetArcTransmissionStateAction.class)) {
startArcAction(true);
}
@@ -900,29 +900,6 @@
}
@ServiceThreadOnly
- private void updateArcFeatureStatus(int portId, boolean isConnected) {
- assertRunOnServiceThread();
- HdmiPortInfo portInfo = mService.getPortInfo(portId);
- if (!portInfo.isArcSupported()) {
- return;
- }
- HdmiDeviceInfo avr = getAvrDeviceInfo();
- if (avr == null) {
- if (isConnected) {
- // Update the status (since TV may not have seen AVR yet) so
- // that ARC can be initiated after discovery.
- mArcFeatureEnabled.put(portId, isConnected);
- }
- return;
- }
- // HEAC 2.4, HEACT 5-15
- // Should not activate ARC if +5V status is false.
- if (avr.getPortId() == portId) {
- changeArcFeatureEnabled(portId, isConnected);
- }
- }
-
- @ServiceThreadOnly
boolean isConnected(int portId) {
assertRunOnServiceThread();
return mService.isConnected(portId);
@@ -952,18 +929,18 @@
@ServiceThreadOnly
void changeArcFeatureEnabled(int portId, boolean enabled) {
assertRunOnServiceThread();
-
- if (mArcFeatureEnabled.get(portId) != enabled) {
- mArcFeatureEnabled.put(portId, enabled);
- if (enabled) {
- if (!mArcEstablished) {
- startArcAction(true);
- }
- } else {
- if (mArcEstablished) {
- startArcAction(false);
- }
- }
+ if (mArcFeatureEnabled.get(portId) == enabled) {
+ return;
+ }
+ mArcFeatureEnabled.put(portId, enabled);
+ HdmiDeviceInfo avr = getAvrDeviceInfo();
+ if (avr == null || avr.getPortId() != portId) {
+ return;
+ }
+ if (enabled && !mArcEstablished) {
+ startArcAction(true);
+ } else if (!enabled && mArcEstablished) {
+ startArcAction(false);
}
}
@@ -1097,14 +1074,14 @@
return true;
}
- private boolean canStartArcUpdateAction(int avrAddress, boolean shouldCheckArcFeatureEnabled) {
+ private boolean canStartArcUpdateAction(int avrAddress, boolean enabled) {
HdmiDeviceInfo avr = getAvrDeviceInfo();
if (avr != null
&& (avrAddress == avr.getLogicalAddress())
&& isConnectedToArcPort(avr.getPhysicalAddress())
&& isDirectConnectAddress(avr.getPhysicalAddress())) {
- if (shouldCheckArcFeatureEnabled) {
- return isArcFeatureEnabled(avr.getPortId());
+ if (enabled) {
+ return isConnected(avr.getPortId()) && isArcFeatureEnabled(avr.getPortId());
} else {
return true;
}
@@ -1566,7 +1543,6 @@
// It covers seq #40, #43.
hotplugActions.get(0).pollAllDevicesNow();
}
- updateArcFeatureStatus(portId, connected);
}
private void removeCecSwitches(int portId) {
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index c9c855b..7362a51 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -10639,12 +10639,30 @@
int flags = permissionState != null
? permissionState.getFlags() : 0;
if (origPermissions.hasRuntimePermission(bp.name, userId)) {
- if (permissionsState.grantRuntimePermission(bp, userId) ==
- PermissionsState.PERMISSION_OPERATION_FAILURE) {
- // If we cannot put the permission as it was, we have to write.
+ // Don't propagate the permission in a permission review mode if
+ // the former was revoked, i.e. marked to not propagate on upgrade.
+ // Note that in a permission review mode install permissions are
+ // represented as constantly granted runtime ones since we need to
+ // keep a per user state associated with the permission. Also the
+ // revoke on upgrade flag is no longer applicable and is reset.
+ final boolean revokeOnUpgrade = (flags & PackageManager
+ .FLAG_PERMISSION_REVOKE_ON_UPGRADE) != 0;
+ if (revokeOnUpgrade) {
+ flags &= ~PackageManager.FLAG_PERMISSION_REVOKE_ON_UPGRADE;
+ // Since we changed the flags, we have to write.
changedRuntimePermissionUserIds = ArrayUtils.appendInt(
changedRuntimePermissionUserIds, userId);
}
+ if (!mPermissionReviewRequired || !revokeOnUpgrade) {
+ if (permissionsState.grantRuntimePermission(bp, userId) ==
+ PermissionsState.PERMISSION_OPERATION_FAILURE) {
+ // If we cannot put the permission as it was,
+ // we have to write.
+ changedRuntimePermissionUserIds = ArrayUtils.appendInt(
+ changedRuntimePermissionUserIds, userId);
+ }
+ }
+
// If the app supports runtime permissions no need for a review.
if (mPermissionReviewRequired
&& appSupportsRuntimePermissions
@@ -13018,8 +13036,10 @@
+ " is not installer for " + packageName);
}
- ps.categoryHint = categoryHint;
- scheduleWriteSettingsLocked();
+ if (ps.categoryHint != categoryHint) {
+ ps.categoryHint = categoryHint;
+ scheduleWriteSettingsLocked();
+ }
}
}
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index bdd1a0f51..040188d 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -17,7 +17,6 @@
package com.android.server.devicepolicy;
import static android.Manifest.permission.MANAGE_CA_CERTIFICATES;
-import static android.app.admin.DevicePolicyManager.ACTION_SHOW_DEVICE_MONITORING_DIALOG;
import static android.app.admin.DevicePolicyManager.CODE_ACCOUNTS_NOT_EMPTY;
import static android.app.admin.DevicePolicyManager.CODE_ADD_MANAGED_PROFILE_DISALLOWED;
import static android.app.admin.DevicePolicyManager.CODE_CANNOT_ADD_MANAGED_PROFILE;
@@ -49,7 +48,6 @@
import android.accessibilityservice.AccessibilityServiceInfo;
import android.accounts.Account;
import android.accounts.AccountManager;
-import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
@@ -180,8 +178,6 @@
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
import java.nio.charset.StandardCharsets;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
@@ -337,7 +333,8 @@
private static final long MINIMUM_STRONG_AUTH_TIMEOUT_MS = 1 * 60 * 60 * 1000; // 1h
/**
- * Strings logged with {@link #PROVISIONING_ENTRY_POINT_ADB}.
+ * Strings logged with {@link
+ * com.android.internal.logging.nano.MetricsProto.MetricsEvent#PROVISIONING_ENTRY_POINT_ADB}.
*/
private static final String LOG_TAG_PROFILE_OWNER = "profile-owner";
private static final String LOG_TAG_DEVICE_OWNER = "device-owner";
@@ -552,11 +549,25 @@
}
if (Intent.ACTION_USER_ADDED.equals(action)) {
sendUserAddedOrRemovedCommand(DeviceAdminReceiver.ACTION_USER_ADDED, userHandle);
- disableDeviceOwnerManagedSingleUserFeaturesIfNeeded();
+ synchronized (DevicePolicyManagerService.this) {
+ // It might take a while for the user to become affiliated. Make security
+ // and network logging unavailable in the meantime.
+ maybePauseDeviceWideLoggingLocked();
+ }
} else if (Intent.ACTION_USER_REMOVED.equals(action)) {
sendUserAddedOrRemovedCommand(DeviceAdminReceiver.ACTION_USER_REMOVED, userHandle);
- disableDeviceOwnerManagedSingleUserFeaturesIfNeeded();
- removeUserData(userHandle);
+ synchronized (DevicePolicyManagerService.this) {
+ // Check whether the user is affiliated, *before* removing its data.
+ boolean isRemovedUserAffiliated = isUserAffiliatedWithDeviceLocked(userHandle);
+ removeUserData(userHandle);
+ if (!isRemovedUserAffiliated) {
+ // We discard the logs when unaffiliated users are deleted (so that the
+ // device owner cannot retrieve data about that user after it's gone).
+ discardDeviceWideLogsLocked();
+ // Resume logging if all remaining users are affiliated.
+ maybeResumeDeviceWideLoggingLocked();
+ }
+ }
} else if (Intent.ACTION_USER_STARTED.equals(action)) {
synchronized (DevicePolicyManagerService.this) {
// Reset the policy data
@@ -1858,9 +1869,10 @@
if (mOwners.hasDeviceOwner()) {
mInjector.systemPropertiesSet(PROPERTY_DEVICE_OWNER_PRESENT, "true");
Slog.i(LOG_TAG, "Set ro.device_owner property to true");
- disableDeviceOwnerManagedSingleUserFeaturesIfNeeded();
+
if (mInjector.securityLogGetLoggingEnabledProperty()) {
mSecurityLogMonitor.start();
+ maybePauseDeviceWideLoggingLocked();
}
} else {
mInjector.systemPropertiesSet(PROPERTY_DEVICE_OWNER_PRESENT, "false");
@@ -3155,7 +3167,7 @@
}
// It's temporary solution to clear DISALLOW_ADD_USER after CTS
- // TODO: b/31952368 when the restriction is moved from system to the device owner,
+ // STOPSHIP(b/31952368) when the restriction is moved from system to the device owner,
// it can be removed.
private void clearDeviceOwnerUserRestrictionLocked(UserHandle userHandle) {
if (mUserManager.hasUserRestriction(UserManager.DISALLOW_ADD_USER, userHandle)) {
@@ -5650,34 +5662,12 @@
}
}
- private boolean isDeviceOwnerManagedSingleUserDevice() {
- synchronized (this) {
- if (!mOwners.hasDeviceOwner()) {
- return false;
- }
- }
- final long callingIdentity = mInjector.binderClearCallingIdentity();
- try {
- if (mInjector.userManagerIsSplitSystemUser()) {
- // In split system user mode, only allow the case where the device owner is managing
- // the only non-system user of the device
- return (mUserManager.getUserCount() == 2
- && mOwners.getDeviceOwnerUserId() != UserHandle.USER_SYSTEM);
- } else {
- return mUserManager.getUserCount() == 1;
- }
- } finally {
- mInjector.binderRestoreCallingIdentity(callingIdentity);
- }
- }
-
- private void ensureDeviceOwnerManagingSingleUser(ComponentName who) throws SecurityException {
+ private void ensureDeviceOwnerAndAllUsersAffiliated(ComponentName who) throws SecurityException {
synchronized (this) {
getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
- }
- if (!isDeviceOwnerManagedSingleUserDevice()) {
- throw new SecurityException(
- "There should only be one user, managed by Device Owner");
+ if (!areAllUsersAffiliatedWithDeviceLocked()) {
+ throw new SecurityException("Not all users are affiliated.");
+ }
}
}
@@ -5687,7 +5677,11 @@
return false;
}
Preconditions.checkNotNull(who, "ComponentName is null");
- ensureDeviceOwnerManagingSingleUser(who);
+
+ // TODO: If an unaffiliated user is removed, the admin will be able to request a bugreport
+ // which could still contain data related to that user. Should we disallow that, e.g. until
+ // next boot? Might not be needed given that this still requires user consent.
+ ensureDeviceOwnerAndAllUsersAffiliated(who);
if (mRemoteBugreportServiceIsActive.get()
|| (getDeviceOwnerRemoteBugreportUri() != null)) {
@@ -5712,7 +5706,8 @@
mRemoteBugreportServiceIsActive.set(true);
mRemoteBugreportSharingAccepted.set(false);
registerRemoteBugreportReceivers();
- mInjector.getNotificationManager().notifyAsUser(LOG_TAG, RemoteBugreportUtils.NOTIFICATION_ID,
+ mInjector.getNotificationManager().notifyAsUser(LOG_TAG,
+ RemoteBugreportUtils.NOTIFICATION_ID,
RemoteBugreportUtils.buildNotification(mContext,
DevicePolicyManager.NOTIFICATION_BUGREPORT_STARTED), UserHandle.ALL);
mHandler.postDelayed(mRemoteBugreportTimeoutRunnable,
@@ -6259,6 +6254,7 @@
admin.userRestrictions = null;
admin.defaultEnabledRestrictionsAlreadySet.clear();
admin.forceEphemeralUsers = false;
+ admin.isNetworkLoggingEnabled = false;
mUserManagerInternal.setForceEphemeralUsers(admin.forceEphemeralUsers);
final DevicePolicyData policyData = getUserData(UserHandle.USER_SYSTEM);
policyData.mLastSecurityLogRetrievalTime = -1;
@@ -6271,7 +6267,11 @@
mOwners.clearDeviceOwner();
mOwners.writeDeviceOwner();
updateDeviceOwnerLocked();
- disableDeviceOwnerManagedSingleUserFeaturesIfNeeded();
+
+ mInjector.securityLogSetLoggingEnabledProperty(false);
+ mSecurityLogMonitor.stop();
+ setNetworkLoggingActiveInternal(false);
+
try {
if (mInjector.getIBackupManager() != null) {
// Reactivate backup service.
@@ -8261,7 +8261,7 @@
synchronized (this) {
getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
final int userHandle = mInjector.userHandleGetCallingUserId();
- if (isUserAffiliatedWithDevice(userHandle)) {
+ if (isUserAffiliatedWithDeviceLocked(userHandle)) {
setLockTaskPackagesLocked(userHandle, new ArrayList<>(Arrays.asList(packages)));
} else {
throw new SecurityException("Admin " + who +
@@ -9442,6 +9442,12 @@
getUserData(UserHandle.USER_SYSTEM).mAffiliationIds = affiliationIds;
saveSettingsLocked(UserHandle.USER_SYSTEM);
}
+
+ // Affiliation status for any user, not just the calling user, might have changed.
+ // The device owner user will still be affiliated after changing its affiliation ids,
+ // but as a result of that other users might become affiliated or un-affiliated.
+ maybePauseDeviceWideLoggingLocked();
+ maybeResumeDeviceWideLoggingLocked();
}
}
@@ -9461,84 +9467,78 @@
@Override
public boolean isAffiliatedUser() {
- return isUserAffiliatedWithDevice(mInjector.userHandleGetCallingUserId());
+ if (!mHasFeature) {
+ return false;
+ }
+
+ synchronized (this) {
+ return isUserAffiliatedWithDeviceLocked(mInjector.userHandleGetCallingUserId());
+ }
}
- private boolean isUserAffiliatedWithDevice(int userId) {
- synchronized (this) {
- if (!mOwners.hasDeviceOwner()) {
- return false;
- }
- if (userId == mOwners.getDeviceOwnerUserId()) {
- // The user that the DO is installed on is always affiliated with the device.
+ private boolean isUserAffiliatedWithDeviceLocked(int userId) {
+ if (!mOwners.hasDeviceOwner()) {
+ return false;
+ }
+ if (userId == mOwners.getDeviceOwnerUserId()) {
+ // The user that the DO is installed on is always affiliated with the device.
+ return true;
+ }
+ if (userId == UserHandle.USER_SYSTEM) {
+ // The system user is always affiliated in a DO device, even if the DO is set on a
+ // different user. This could be the case if the DO is set in the primary user
+ // of a split user device.
+ return true;
+ }
+ final ComponentName profileOwner = getProfileOwner(userId);
+ if (profileOwner == null) {
+ return false;
+ }
+ final Set<String> userAffiliationIds = getUserData(userId).mAffiliationIds;
+ final Set<String> deviceAffiliationIds =
+ getUserData(UserHandle.USER_SYSTEM).mAffiliationIds;
+ for (String id : userAffiliationIds) {
+ if (deviceAffiliationIds.contains(id)) {
return true;
}
- if (userId == UserHandle.USER_SYSTEM) {
- // The system user is always affiliated in a DO device, even if the DO is set on a
- // different user. This could be the case if the DO is set in the primary user
- // of a split user device.
- return true;
- }
- final ComponentName profileOwner = getProfileOwner(userId);
- if (profileOwner == null) {
- return false;
- }
- final Set<String> userAffiliationIds = getUserData(userId).mAffiliationIds;
- final Set<String> deviceAffiliationIds =
- getUserData(UserHandle.USER_SYSTEM).mAffiliationIds;
- for (String id : userAffiliationIds) {
- if (deviceAffiliationIds.contains(id)) {
- return true;
- }
- }
}
return false;
}
- private synchronized void disableDeviceOwnerManagedSingleUserFeaturesIfNeeded() {
- final boolean isSingleUserManagedDevice = isDeviceOwnerManagedSingleUserDevice();
-
- // disable security logging if needed
- if (!isSingleUserManagedDevice) {
- mInjector.securityLogSetLoggingEnabledProperty(false);
- Slog.w(LOG_TAG, "Security logging turned off as it's no longer a single user managed"
- + " device.");
- }
-
- // disable backup service if needed
- // note: when clearing DO, the backup service shouldn't be disabled if it was enabled by
- // the device owner
- if (mOwners.hasDeviceOwner() && !isSingleUserManagedDevice) {
- setBackupServiceEnabledInternal(false);
- Slog.w(LOG_TAG, "Backup is off as it's a managed device that has more that one user.");
- }
-
- // disable network logging if needed
- if (!isSingleUserManagedDevice) {
- setNetworkLoggingActiveInternal(false);
- Slog.w(LOG_TAG, "Network logging turned off as it's no longer a single user managed"
- + " device.");
- // if there still is a device owner, disable logging policy, otherwise the admin
- // has been nuked
- if (mOwners.hasDeviceOwner()) {
- getDeviceOwnerAdminLocked().isNetworkLoggingEnabled = false;
- saveSettingsLocked(mOwners.getDeviceOwnerUserId());
+ private boolean areAllUsersAffiliatedWithDeviceLocked() {
+ final long ident = mInjector.binderClearCallingIdentity();
+ try {
+ final List<UserInfo> userInfos = mUserManager.getUsers();
+ for (int i = 0; i < userInfos.size(); i++) {
+ int userId = userInfos.get(i).id;
+ if (!isUserAffiliatedWithDeviceLocked(userId)) {
+ Slog.d(LOG_TAG, "User id " + userId + " not affiliated.");
+ return false;
+ }
}
+ } finally {
+ mInjector.binderRestoreCallingIdentity(ident);
}
+
+ return true;
}
@Override
public void setSecurityLoggingEnabled(ComponentName admin, boolean enabled) {
+ if (!mHasFeature) {
+ return;
+ }
Preconditions.checkNotNull(admin);
- ensureDeviceOwnerManagingSingleUser(admin);
synchronized (this) {
+ getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
if (enabled == mInjector.securityLogGetLoggingEnabledProperty()) {
return;
}
mInjector.securityLogSetLoggingEnabledProperty(enabled);
if (enabled) {
mSecurityLogMonitor.start();
+ maybePauseDeviceWideLoggingLocked();
} else {
mSecurityLogMonitor.stop();
}
@@ -9547,6 +9547,10 @@
@Override
public boolean isSecurityLoggingEnabled(ComponentName admin) {
+ if (!mHasFeature) {
+ return false;
+ }
+
Preconditions.checkNotNull(admin);
synchronized (this) {
getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
@@ -9565,10 +9569,15 @@
@Override
public ParceledListSlice<SecurityEvent> retrievePreRebootSecurityLogs(ComponentName admin) {
- Preconditions.checkNotNull(admin);
- ensureDeviceOwnerManagingSingleUser(admin);
+ if (!mHasFeature) {
+ return null;
+ }
- if (!mContext.getResources().getBoolean(R.bool.config_supportPreRebootSecurityLogs)) {
+ Preconditions.checkNotNull(admin);
+ ensureDeviceOwnerAndAllUsersAffiliated(admin);
+
+ if (!mContext.getResources().getBoolean(R.bool.config_supportPreRebootSecurityLogs)
+ || !mInjector.securityLogGetLoggingEnabledProperty()) {
return null;
}
@@ -9586,8 +9595,16 @@
@Override
public ParceledListSlice<SecurityEvent> retrieveSecurityLogs(ComponentName admin) {
+ if (!mHasFeature) {
+ return null;
+ }
+
Preconditions.checkNotNull(admin);
- ensureDeviceOwnerManagingSingleUser(admin);
+ ensureDeviceOwnerAndAllUsersAffiliated(admin);
+
+ if (!mInjector.securityLogGetLoggingEnabledProperty()) {
+ return null;
+ }
recordSecurityLogRetrievalTime();
@@ -9794,18 +9811,21 @@
}
}
+ // TODO(b/22388012): When backup is available for secondary users and profiles, consider
+ // whether there are any privacy/security implications of enabling the backup service here
+ // if there are other users or profiles unmanaged or managed by a different entity (i.e. not
+ // affiliated).
@Override
public void setBackupServiceEnabled(ComponentName admin, boolean enabled) {
- Preconditions.checkNotNull(admin);
if (!mHasFeature) {
return;
}
- ensureDeviceOwnerManagingSingleUser(admin);
- setBackupServiceEnabledInternal(enabled);
- }
+ Preconditions.checkNotNull(admin);
+ synchronized (this) {
+ getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
+ }
- private synchronized void setBackupServiceEnabledInternal(boolean enabled) {
- long ident = mInjector.binderClearCallingIdentity();
+ final long ident = mInjector.binderClearCallingIdentity();
try {
IBackupManager ibm = mInjector.getIBackupManager();
if (ibm != null) {
@@ -9906,7 +9926,7 @@
final boolean isCallerDeviceOwner = isDeviceOwner(callingOwner);
final boolean isCallerManagedProfile = isManagedProfile(callingUserId);
if ((!isCallerDeviceOwner && !isCallerManagedProfile)
- || !isUserAffiliatedWithDevice(callingUserId)) {
+ || !isUserAffiliatedWithDeviceLocked(callingUserId)) {
return targetUsers;
}
@@ -9926,7 +9946,7 @@
// Both must be the same package and be affiliated in order to bind.
if (callingOwnerPackage.equals(targetOwnerPackage)
- && isUserAffiliatedWithDevice(userId)) {
+ && isUserAffiliatedWithDeviceLocked(userId)) {
targetUsers.add(UserHandle.of(userId));
}
}
@@ -10024,7 +10044,7 @@
return;
}
Preconditions.checkNotNull(admin);
- ensureDeviceOwnerManagingSingleUser(admin);
+ getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
if (enabled == isNetworkLoggingEnabledInternalLocked()) {
// already in the requested state
@@ -10051,10 +10071,10 @@
Slog.wtf(LOG_TAG, "Network logging could not be started due to the logging"
+ " service not being available yet.");
}
+ maybePauseDeviceWideLoggingLocked();
sendNetworkLoggingNotificationLocked();
} else {
if (mNetworkLogger != null && !mNetworkLogger.stopNetworkLogging()) {
- mNetworkLogger = null;
Slog.wtf(LOG_TAG, "Network logging could not be stopped due to the logging"
+ " service not being available yet.");
}
@@ -10066,6 +10086,44 @@
}
}
+ /** Pauses security and network logging if there are unaffiliated users on the device */
+ private void maybePauseDeviceWideLoggingLocked() {
+ if (!areAllUsersAffiliatedWithDeviceLocked()) {
+ Slog.i(LOG_TAG, "There are unaffiliated users, security and network logging will be "
+ + "paused if enabled.");
+ mSecurityLogMonitor.pause();
+ if (mNetworkLogger != null) {
+ mNetworkLogger.pause();
+ }
+ }
+ }
+
+ /** Resumes security and network logging (if they are enabled) if all users are affiliated */
+ private void maybeResumeDeviceWideLoggingLocked() {
+ if (areAllUsersAffiliatedWithDeviceLocked()) {
+ final long ident = mInjector.binderClearCallingIdentity();
+ try {
+ mSecurityLogMonitor.resume();
+ if (mNetworkLogger != null) {
+ mNetworkLogger.resume();
+ }
+ } finally {
+ mInjector.binderRestoreCallingIdentity(ident);
+ }
+ }
+ }
+
+ /** Deletes any security and network logs that might have been collected so far */
+ private void discardDeviceWideLogsLocked() {
+ mSecurityLogMonitor.discardLogs();
+ if (mNetworkLogger != null) {
+ mNetworkLogger.discardLogs();
+ }
+ // TODO: We should discard pre-boot security logs here too, as otherwise those
+ // logs (which might contain data from the user just removed) will be
+ // available after next boot.
+ }
+
@Override
public boolean isNetworkLoggingEnabled(ComponentName admin) {
if (!mHasFeature) {
@@ -10090,32 +10148,27 @@
* @see NetworkLoggingHandler#MAX_EVENTS_PER_BATCH
*/
@Override
- public synchronized List<NetworkEvent> retrieveNetworkLogs(ComponentName admin,
- long batchToken) {
+ public List<NetworkEvent> retrieveNetworkLogs(ComponentName admin, long batchToken) {
if (!mHasFeature) {
return null;
}
Preconditions.checkNotNull(admin);
- ensureDeviceOwnerManagingSingleUser(admin);
+ ensureDeviceOwnerAndAllUsersAffiliated(admin);
- if (mNetworkLogger == null) {
- return null;
- }
-
- if (!isNetworkLoggingEnabledInternalLocked()) {
- return null;
- }
-
- final long currentTime = System.currentTimeMillis();
synchronized (this) {
+ if (mNetworkLogger == null
+ || !isNetworkLoggingEnabledInternalLocked()) {
+ return null;
+ }
+
+ final long currentTime = System.currentTimeMillis();
DevicePolicyData policyData = getUserData(UserHandle.USER_SYSTEM);
if (currentTime > policyData.mLastNetworkLogsRetrievalTime) {
policyData.mLastNetworkLogsRetrievalTime = currentTime;
saveSettingsLocked(UserHandle.USER_SYSTEM);
}
+ return mNetworkLogger.retrieveLogs(batchToken);
}
-
- return mNetworkLogger.retrieveLogs(batchToken);
}
private void sendNetworkLoggingNotificationLocked() {
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/NetworkLogger.java b/services/devicepolicy/java/com/android/server/devicepolicy/NetworkLogger.java
index b82cb3c..0085931 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/NetworkLogger.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/NetworkLogger.java
@@ -31,7 +31,6 @@
import com.android.server.ServiceThread;
-import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -130,6 +129,8 @@
Log.d(TAG, "Stopping network logging");
// stop the logging regardless of whether we fail to unregister listener
mIsLoggingEnabled.set(false);
+ discardLogs();
+
try {
if (!checkIpConnectivityMetricsService()) {
// the IIpConnectivityMetrics service should have been present at this point
@@ -140,9 +141,43 @@
return mIpConnectivityMetrics.unregisterNetdEventCallback();
} catch (RemoteException re) {
Slog.wtf(TAG, "Failed to make remote calls to unregister the callback", re);
- } finally {
- mHandlerThread.quitSafely();
return true;
+ } finally {
+ if (mHandlerThread != null) {
+ mHandlerThread.quitSafely();
+ }
+ }
+ }
+
+ /**
+ * If logs are being collected, keep collecting them but stop notifying the device owner that
+ * new logs are available (since they cannot be retrieved)
+ */
+ void pause() {
+ if (mNetworkLoggingHandler != null) {
+ mNetworkLoggingHandler.pause();
+ }
+ }
+
+ /**
+ * If logs are being collected, start notifying the device owner when logs are ready to be
+ * collected again (if it was paused).
+ * <p>If logging is enabled and there are logs ready to be retrieved, this method will attempt
+ * to notify the device owner. Therefore calling identity should be cleared before calling it
+ * (in case the method is called from a user other than the DO's user).
+ */
+ void resume() {
+ if (mNetworkLoggingHandler != null) {
+ mNetworkLoggingHandler.resume();
+ }
+ }
+
+ /**
+ * Discard all collected logs.
+ */
+ void discardLogs() {
+ if (mNetworkLoggingHandler != null) {
+ mNetworkLoggingHandler.discardLogs();
}
}
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/NetworkLoggingHandler.java b/services/devicepolicy/java/com/android/server/devicepolicy/NetworkLoggingHandler.java
index baa4c13..7d68412 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/NetworkLoggingHandler.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/NetworkLoggingHandler.java
@@ -55,10 +55,16 @@
@GuardedBy("this")
private ArrayList<NetworkEvent> mFullBatch;
- // each full batch is represented by its token, which the DPC has to provide back to revieve it
+ @GuardedBy("this")
+ private boolean mPaused = false;
+
+ // each full batch is represented by its token, which the DPC has to provide back to retrieve it
@GuardedBy("this")
private long mCurrentFullBatchToken;
+ @GuardedBy("this")
+ private long mLastRetrievedFullBatchToken;
+
NetworkLoggingHandler(Looper looper, DevicePolicyManagerService dpm) {
super(looper);
mDpm = dpm;
@@ -70,15 +76,19 @@
case LOG_NETWORK_EVENT_MSG: {
NetworkEvent networkEvent = msg.getData().getParcelable(NETWORK_EVENT_KEY);
if (networkEvent != null) {
- mNetworkEvents.add(networkEvent);
- if (mNetworkEvents.size() >= MAX_EVENTS_PER_BATCH) {
- finalizeBatchAndNotifyDeviceOwnerIfNotEmpty();
+ synchronized (NetworkLoggingHandler.this) {
+ mNetworkEvents.add(networkEvent);
+ if (mNetworkEvents.size() >= MAX_EVENTS_PER_BATCH) {
+ finalizeBatchAndNotifyDeviceOwnerLocked();
+ }
}
}
break;
}
case FINALIZE_BATCH_MSG: {
- finalizeBatchAndNotifyDeviceOwnerIfNotEmpty();
+ synchronized (NetworkLoggingHandler.this) {
+ finalizeBatchAndNotifyDeviceOwnerLocked();
+ }
break;
}
}
@@ -91,22 +101,49 @@
+ "ms from now.");
}
- private synchronized void finalizeBatchAndNotifyDeviceOwnerIfNotEmpty() {
+ synchronized void pause() {
+ Log.d(TAG, "Paused network logging");
+ mPaused = true;
+ }
+
+ synchronized void resume() {
+ if (!mPaused) {
+ Log.d(TAG, "Attempted to resume network logging, but logging is not paused.");
+ return;
+ }
+
+ Log.d(TAG, "Resumed network logging. Current batch="
+ + mCurrentFullBatchToken + ", LastRetrievedBatch=" + mLastRetrievedFullBatchToken);
+ mPaused = false;
+
+ // If there is a full batch ready that the device owner hasn't been notified about, do it
+ // now.
+ if (mFullBatch != null && mFullBatch.size() > 0
+ && mLastRetrievedFullBatchToken != mCurrentFullBatchToken) {
+ scheduleBatchFinalization();
+ notifyDeviceOwnerLocked();
+ }
+ }
+
+ synchronized void discardLogs() {
+ mFullBatch = null;
+ mNetworkEvents = new ArrayList<NetworkEvent>();
+ Log.d(TAG, "Discarded all network logs");
+ }
+
+ @GuardedBy("this")
+ private void finalizeBatchAndNotifyDeviceOwnerLocked() {
if (mNetworkEvents.size() > 0) {
// finalize the batch and start a new one from scratch
mFullBatch = mNetworkEvents;
mCurrentFullBatchToken++;
mNetworkEvents = new ArrayList<NetworkEvent>();
- // notify DO that there's a new non-empty batch waiting
- Bundle extras = new Bundle();
- extras.putLong(DeviceAdminReceiver.EXTRA_NETWORK_LOGS_TOKEN, mCurrentFullBatchToken);
- extras.putInt(DeviceAdminReceiver.EXTRA_NETWORK_LOGS_COUNT, mFullBatch.size());
- Log.d(TAG, "Sending network logging batch broadcast to device owner, batchToken: "
- + mCurrentFullBatchToken);
- mDpm.sendDeviceOwnerCommand(DeviceAdminReceiver.ACTION_NETWORK_LOGS_AVAILABLE, extras);
+ if (!mPaused) {
+ notifyDeviceOwnerLocked();
+ }
} else {
// don't notify the DO, since there are no events; DPC can still retrieve
- // the last full batch
+ // the last full batch if not paused.
Log.d(TAG, "Was about to finalize the batch, but there were no events to send to"
+ " the DPC, the batchToken of last available batch: "
+ mCurrentFullBatchToken);
@@ -115,10 +152,21 @@
scheduleBatchFinalization();
}
+ @GuardedBy("this")
+ private void notifyDeviceOwnerLocked() {
+ Bundle extras = new Bundle();
+ extras.putLong(DeviceAdminReceiver.EXTRA_NETWORK_LOGS_TOKEN, mCurrentFullBatchToken);
+ extras.putInt(DeviceAdminReceiver.EXTRA_NETWORK_LOGS_COUNT, mFullBatch.size());
+ Log.d(TAG, "Sending network logging batch broadcast to device owner, batchToken: "
+ + mCurrentFullBatchToken);
+ mDpm.sendDeviceOwnerCommand(DeviceAdminReceiver.ACTION_NETWORK_LOGS_AVAILABLE, extras);
+ }
+
synchronized List<NetworkEvent> retrieveFullLogBatch(long batchToken) {
if (batchToken != mCurrentFullBatchToken) {
return null;
}
+ mLastRetrievedFullBatchToken = mCurrentFullBatchToken;
return mFullBatch;
}
}
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/SecurityLogMonitor.java b/services/devicepolicy/java/com/android/server/devicepolicy/SecurityLogMonitor.java
index 79702a8..18f06be 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/SecurityLogMonitor.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/SecurityLogMonitor.java
@@ -19,6 +19,7 @@
import android.app.admin.DeviceAdminReceiver;
import android.app.admin.SecurityLog;
import android.app.admin.SecurityLog.SecurityEvent;
+import android.os.SystemClock;
import android.util.Log;
import android.util.Slog;
@@ -50,7 +51,7 @@
mService = service;
}
- private static final boolean DEBUG = false;
+ private static final boolean DEBUG = false; // STOPSHIP if true.
private static final String TAG = "SecurityLogMonitor";
/**
* Each log entry can hold up to 4K bytes (but as of {@link android.os.Build.VERSION_CODES#N}
@@ -78,17 +79,25 @@
private ArrayList<SecurityEvent> mPendingLogs = new ArrayList<SecurityEvent>();
@GuardedBy("mLock")
private boolean mAllowedToRetrieve = false;
- // When DO will be allowed to retrieves the log, in milliseconds.
+
+ /**
+ * When DO will be allowed to retrieve the log, in milliseconds since boot (as per
+ * {@link SystemClock#elapsedRealtime()})
+ */
@GuardedBy("mLock")
- private long mNextAllowedRetrivalTimeMillis = -1;
+ private long mNextAllowedRetrievalTimeMillis = -1;
+ @GuardedBy("mLock")
+ private boolean mPaused = false;
void start() {
+ Slog.i(TAG, "Starting security logging.");
mLock.lock();
try {
if (mMonitorThread == null) {
mPendingLogs = new ArrayList<SecurityEvent>();
mAllowedToRetrieve = false;
- mNextAllowedRetrivalTimeMillis = -1;
+ mNextAllowedRetrievalTimeMillis = -1;
+ mPaused = false;
mMonitorThread = new Thread(this);
mMonitorThread.start();
@@ -99,6 +108,7 @@
}
void stop() {
+ Slog.i(TAG, "Stopping security logging.");
mLock.lock();
try {
if (mMonitorThread != null) {
@@ -111,7 +121,8 @@
// Reset state and clear buffer
mPendingLogs = new ArrayList<SecurityEvent>();
mAllowedToRetrieve = false;
- mNextAllowedRetrivalTimeMillis = -1;
+ mNextAllowedRetrievalTimeMillis = -1;
+ mPaused = false;
mMonitorThread = null;
}
} finally {
@@ -120,6 +131,58 @@
}
/**
+ * If logs are being collected, keep collecting them but stop notifying the device owner that
+ * new logs are available (since they cannot be retrieved).
+ */
+ void pause() {
+ Slog.i(TAG, "Paused.");
+
+ mLock.lock();
+ mPaused = true;
+ mAllowedToRetrieve = false;
+ mLock.unlock();
+ }
+
+ /**
+ * If logs are being collected, start notifying the device owner when logs are ready to be
+ * retrieved again (if it was paused).
+ * <p>If logging is enabled and there are logs ready to be retrieved, this method will attempt
+ * to notify the device owner. Therefore calling identity should be cleared before calling it
+ * (in case the method is called from a user other than the DO's user).
+ */
+ void resume() {
+ mLock.lock();
+ try {
+ if (!mPaused) {
+ Log.d(TAG, "Attempted to resume, but logging is not paused.");
+ return;
+ }
+ mPaused = false;
+ mAllowedToRetrieve = false;
+ } finally {
+ mLock.unlock();
+ }
+
+ Slog.i(TAG, "Resumed.");
+ try {
+ notifyDeviceOwnerIfNeeded();
+ } catch (InterruptedException e) {
+ Log.w(TAG, "Thread interrupted.", e);
+ }
+ }
+
+ /**
+ * Discard all collected logs.
+ */
+ void discardLogs() {
+ mLock.lock();
+ mAllowedToRetrieve = false;
+ mPendingLogs = new ArrayList<SecurityEvent>();
+ mLock.unlock();
+ Slog.i(TAG, "Discarded all logs.");
+ }
+
+ /**
* Returns the new batch of logs since the last call to this method. Returns null if
* rate limit is exceeded.
*/
@@ -128,7 +191,7 @@
try {
if (mAllowedToRetrieve) {
mAllowedToRetrieve = false;
- mNextAllowedRetrivalTimeMillis = System.currentTimeMillis()
+ mNextAllowedRetrievalTimeMillis = SystemClock.elapsedRealtime()
+ RATE_LIMIT_INTERVAL_MILLISECONDS;
List<SecurityEvent> result = mPendingLogs;
mPendingLogs = new ArrayList<SecurityEvent>();
@@ -163,7 +226,7 @@
SecurityLog.readEventsSince(lastLogTimestampNanos + 1, logs);
}
if (!logs.isEmpty()) {
- if (DEBUG) Slog.d(TAG, "processing new logs");
+ if (DEBUG) Slog.d(TAG, "processing new logs. Events: " + logs.size());
mLock.lockInterruptibly();
try {
mPendingLogs.addAll(logs);
@@ -172,6 +235,7 @@
mPendingLogs = new ArrayList<SecurityEvent>(mPendingLogs.subList(
mPendingLogs.size() - (BUFFER_ENTRIES_MAXIMUM_LEVEL / 2),
mPendingLogs.size()));
+ Slog.i(TAG, "Pending logs buffer full. Discarding old logs.");
}
} finally {
mLock.unlock();
@@ -188,7 +252,7 @@
break;
}
}
- if (DEBUG) Slog.d(TAG, "MonitorThread exit.");
+ Slog.i(TAG, "MonitorThread exit.");
}
private void notifyDeviceOwnerIfNeeded() throws InterruptedException {
@@ -196,15 +260,24 @@
boolean allowToRetrieveNow = false;
mLock.lockInterruptibly();
try {
+ if (mPaused) {
+ return;
+ }
+
+ // STOPSHIP(b/34186771): If the previous notification didn't reach the DO and logs were
+ // not retrieved (e.g. the broadcast was sent before the user was unlocked), no more
+ // subsequent callbacks will be sent. We should make sure that the DO gets notified
+ // before logs are lost.
int logSize = mPendingLogs.size();
if (logSize >= BUFFER_ENTRIES_NOTIFICATION_LEVEL) {
// Allow DO to retrieve logs if too many pending logs
allowToRetrieveNow = true;
+ if (DEBUG) Slog.d(TAG, "Number of log entries over threshold: " + logSize);
} else if (logSize > 0) {
- if (mNextAllowedRetrivalTimeMillis == -1 ||
- System.currentTimeMillis() >= mNextAllowedRetrivalTimeMillis) {
+ if (SystemClock.elapsedRealtime() >= mNextAllowedRetrievalTimeMillis) {
// Rate limit reset
allowToRetrieveNow = true;
+ if (DEBUG) Slog.d(TAG, "Timeout reached");
}
}
shouldNotifyDO = (!mAllowedToRetrieve) && allowToRetrieveNow;
@@ -213,7 +286,7 @@
mLock.unlock();
}
if (shouldNotifyDO) {
- if (DEBUG) Slog.d(TAG, "notify DO");
+ Slog.i(TAG, "notify DO");
mService.sendDeviceOwnerCommand(DeviceAdminReceiver.ACTION_SECURITY_LOGS_AVAILABLE,
null);
}
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 68cb0c5..08fb591 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -175,6 +175,8 @@
"com.google.android.clockwork.bluetooth.WearBluetoothService";
private static final String WEAR_WIFI_MEDIATOR_SERVICE_CLASS =
"com.google.android.clockwork.wifi.WearWifiMediatorService";
+ private static final String WEAR_CELLULAR_MEDIATOR_SERVICE_CLASS =
+ "com.google.android.clockwork.cellular.WearCellularMediatorService";
private static final String WEAR_TIME_SERVICE_CLASS =
"com.google.android.clockwork.time.WearTimeService";
private static final String ACCOUNT_SERVICE_CLASS =
@@ -1381,6 +1383,13 @@
traceBeginAndSlog("StartWearWifiMediator");
mSystemServiceManager.startService(WEAR_WIFI_MEDIATOR_SERVICE_CLASS);
traceEnd();
+
+ if (SystemProperties.getBoolean("config.enable_cellmediator", false)) {
+ traceBeginAndSlog("StartWearCellularMediator");
+ mSystemServiceManager.startService(WEAR_CELLULAR_MEDIATOR_SERVICE_CLASS);
+ traceEnd();
+ }
+
if (!disableNonCoreServices) {
traceBeginAndSlog("StartWearTimeService");
mSystemServiceManager.startService(WEAR_TIME_SERVICE_CLASS);
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
index e68895e..c3eb09d 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
@@ -2845,7 +2845,10 @@
public void testGetLastSecurityLogRetrievalTime() throws Exception {
mContext.binder.callingUid = DpmMockContext.CALLER_SYSTEM_USER_UID;
setupDeviceOwner();
- when(mContext.userManager.getUserCount()).thenReturn(1);
+
+ // setUp() adds a secondary user for CALLER_USER_HANDLE. Remove it as otherwise the
+ // feature is disabled because there are non-affiliated secondary users.
+ mContext.removeUser(DpmMockContext.CALLER_USER_HANDLE);
when(mContext.resources.getBoolean(R.bool.config_supportPreRebootSecurityLogs))
.thenReturn(true);
@@ -2854,6 +2857,10 @@
// Enabling logging should not change the timestamp.
dpm.setSecurityLoggingEnabled(admin1, true);
+ verify(mContext.settings)
+ .securityLogSetLoggingEnabledProperty(true);
+ when(mContext.settings.securityLogGetLoggingEnabledProperty())
+ .thenReturn(true);
assertEquals(-1, dpm.getLastSecurityLogRetrievalTime());
// Retrieving the logs should update the timestamp.
@@ -2906,7 +2913,7 @@
public void testGetLastBugReportRequestTime() throws Exception {
mContext.binder.callingUid = DpmMockContext.CALLER_SYSTEM_USER_UID;
setupDeviceOwner();
- when(mContext.userManager.getUserCount()).thenReturn(1);
+
mContext.packageName = admin1.getPackageName();
mContext.applicationInfo = new ApplicationInfo();
when(mContext.resources.getColor(eq(R.color.notification_action_list), anyObject()))
@@ -2914,6 +2921,10 @@
when(mContext.resources.getColor(eq(R.color.notification_material_background_color),
anyObject())).thenReturn(Color.WHITE);
+ // setUp() adds a secondary user for CALLER_USER_HANDLE. Remove it as otherwise the
+ // feature is disabled because there are non-affiliated secondary users.
+ mContext.removeUser(DpmMockContext.CALLER_USER_HANDLE);
+
// No bug reports were requested so far.
assertEquals(-1, dpm.getLastBugReportRequestTime());
@@ -2951,7 +2962,16 @@
public void testGetLastNetworkLogRetrievalTime() throws Exception {
mContext.binder.callingUid = DpmMockContext.CALLER_SYSTEM_USER_UID;
setupDeviceOwner();
- when(mContext.userManager.getUserCount()).thenReturn(1);
+ mContext.packageName = admin1.getPackageName();
+ mContext.applicationInfo = new ApplicationInfo();
+ when(mContext.resources.getColor(eq(R.color.notification_action_list), anyObject()))
+ .thenReturn(Color.WHITE);
+ when(mContext.resources.getColor(eq(R.color.notification_material_background_color),
+ anyObject())).thenReturn(Color.WHITE);
+
+ // setUp() adds a secondary user for CALLER_USER_HANDLE. Remove it as otherwise the
+ // feature is disabled because there are non-affiliated secondary users.
+ mContext.removeUser(DpmMockContext.CALLER_USER_HANDLE);
when(mContext.iipConnectivityMetrics.registerNetdEventCallback(anyObject()))
.thenReturn(true);
@@ -3149,6 +3169,44 @@
assertNull(dpm.getDeviceOwnerOrganizationName());
}
+ public void testWipeDataManagedProfile() throws Exception {
+ final int MANAGED_PROFILE_USER_ID = 15;
+ final int MANAGED_PROFILE_ADMIN_UID = UserHandle.getUid(MANAGED_PROFILE_USER_ID, 19436);
+ addManagedProfile(admin1, MANAGED_PROFILE_ADMIN_UID, admin1);
+ mContext.binder.callingUid = MANAGED_PROFILE_ADMIN_UID;
+
+ // Even if the caller is the managed profile, the current user is the user 0
+ when(mContext.iactivityManager.getCurrentUser())
+ .thenReturn(new UserInfo(UserHandle.USER_SYSTEM, "user system", 0));
+
+ dpm.wipeData(0);
+ verify(mContext.userManagerInternal).removeUserEvenWhenDisallowed(
+ MANAGED_PROFILE_USER_ID);
+ }
+
+ public void testWipeDataManagedProfileDisallowed() throws Exception {
+ final int MANAGED_PROFILE_USER_ID = 15;
+ final int MANAGED_PROFILE_ADMIN_UID = UserHandle.getUid(MANAGED_PROFILE_USER_ID, 19436);
+ addManagedProfile(admin1, MANAGED_PROFILE_ADMIN_UID, admin1);
+
+ // Even if the caller is the managed profile, the current user is the user 0
+ when(mContext.iactivityManager.getCurrentUser())
+ .thenReturn(new UserInfo(UserHandle.USER_SYSTEM, "user system", 0));
+
+ when(mContext.userManager.getUserRestrictionSource(
+ UserManager.DISALLOW_REMOVE_MANAGED_PROFILE,
+ UserHandle.of(MANAGED_PROFILE_USER_ID)))
+ .thenReturn(UserManager.RESTRICTION_SOURCE_SYSTEM);
+ mContext.binder.callingUid = MANAGED_PROFILE_ADMIN_UID;
+ try {
+ // The PO is not allowed to remove the profile if the user restriction was set on the
+ // profile by the system
+ dpm.wipeData(0);
+ fail("SecurityException not thrown");
+ } catch (SecurityException expected) {
+ }
+ }
+
private void setUserSetupCompleteForUser(boolean isUserSetupComplete, int userhandle) {
when(mContext.settings.settingsSecureGetIntForUser(Settings.Secure.USER_SETUP_COMPLETE, 0,
userhandle)).thenReturn(isUserSetupComplete ? 1 : 0);
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java
index a1b6769..44bf547 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java
@@ -393,6 +393,18 @@
return dir;
}
+ public void removeUser(int userId) {
+ for (int i = 0; i < mUserInfos.size(); i++) {
+ if (mUserInfos.get(i).id == userId) {
+ mUserInfos.remove(i);
+ break;
+ }
+ }
+ when(userManager.getUserInfo(eq(userId))).thenReturn(null);
+
+ when(userManager.isUserRunning(eq(new UserHandle(userId)))).thenReturn(false);
+ }
+
private UserInfo getUserInfo(int userId) {
for (UserInfo ui : mUserInfos) {
if (ui.id == userId) {
diff --git a/services/usage/java/com/android/server/usage/UserUsageStatsService.java b/services/usage/java/com/android/server/usage/UserUsageStatsService.java
index ba770ef..d9f352c 100644
--- a/services/usage/java/com/android/server/usage/UserUsageStatsService.java
+++ b/services/usage/java/com/android/server/usage/UserUsageStatsService.java
@@ -188,7 +188,6 @@
String[] annotations = event.mContentAnnotations;
if (annotations != null) {
for (String annotation : annotations) {
- // TODO(kanlig): update with confidences of annotations.
stats.updateChooserCounts(event.mPackage, annotation, event.mAction);
}
}
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index 861a1eb..eb838fa 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -936,8 +936,10 @@
*
* <p>Requires Permission:
* {@link android.Manifest.permission#READ_PHONE_STATE READ_PHONE_STATE}
+ *
+ * @hide
*/
- /** {@hide} */
+ @SystemApi
public String getImei() {
return getImei(getDefaultSim());
}
@@ -949,8 +951,10 @@
* {@link android.Manifest.permission#READ_PHONE_STATE READ_PHONE_STATE}
*
* @param slotId of which deviceID is returned
+ *
+ * @hide
*/
- /** {@hide} */
+ @SystemApi
public String getImei(int slotId) {
ITelephony telephony = getITelephony();
if (telephony == null) return null;
diff --git a/services/tests/servicestests/src/android/net/util/BlockingSocketReaderTest.java b/tests/net/java/android/net/util/BlockingSocketReaderTest.java
similarity index 100%
rename from services/tests/servicestests/src/android/net/util/BlockingSocketReaderTest.java
rename to tests/net/java/android/net/util/BlockingSocketReaderTest.java
diff --git a/services/tests/servicestests/src/android/net/util/ConnectivityPacketSummaryTest.java b/tests/net/java/android/net/util/ConnectivityPacketSummaryTest.java
similarity index 100%
rename from services/tests/servicestests/src/android/net/util/ConnectivityPacketSummaryTest.java
rename to tests/net/java/android/net/util/ConnectivityPacketSummaryTest.java
diff --git a/tests/net/java/com/android/server/connectivity/tethering/UpstreamNetworkMonitorTest.java b/tests/net/java/com/android/server/connectivity/tethering/UpstreamNetworkMonitorTest.java
new file mode 100644
index 0000000..b2a9a49
--- /dev/null
+++ b/tests/net/java/com/android/server/connectivity/tethering/UpstreamNetworkMonitorTest.java
@@ -0,0 +1,213 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.tethering;
+
+import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.reset;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.ConnectivityManager.NetworkCallback;
+import android.net.IConnectivityManager;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.runner.RunWith;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class UpstreamNetworkMonitorTest {
+ private static final int EVENT_UNM_UPDATE = 1;
+
+ @Mock private Context mContext;
+ @Mock private IConnectivityManager mCS;
+
+ private TestConnectivityManager mCM;
+ private UpstreamNetworkMonitor mUNM;
+
+ @Before public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ reset(mContext);
+ reset(mCS);
+
+ mCM = new TestConnectivityManager(mContext, mCS);
+ mUNM = new UpstreamNetworkMonitor(null, EVENT_UNM_UPDATE, (ConnectivityManager) mCM);
+ }
+
+ @Test
+ public void testDoesNothingBeforeStarted() {
+ UpstreamNetworkMonitor unm = new UpstreamNetworkMonitor(null, null, EVENT_UNM_UPDATE);
+ assertFalse(unm.mobileNetworkRequested());
+ // Given a null Context, and therefore a null ConnectivityManager,
+ // these would cause an exception, if they actually attempted anything.
+ unm.mobileUpstreamRequiresDun(true);
+ unm.mobileUpstreamRequiresDun(false);
+ }
+
+ @Test
+ public void testDefaultNetworkIsTracked() throws Exception {
+ assertEquals(0, mCM.trackingDefault.size());
+
+ mUNM.start();
+ assertEquals(1, mCM.trackingDefault.size());
+
+ mUNM.stop();
+ assertTrue(mCM.isEmpty());
+ }
+
+ @Test
+ public void testListensForDunNetworks() throws Exception {
+ assertTrue(mCM.listening.isEmpty());
+
+ mUNM.start();
+ assertFalse(mCM.listening.isEmpty());
+ assertTrue(mCM.isListeningForDun());
+
+ mUNM.stop();
+ assertTrue(mCM.isEmpty());
+ }
+
+ @Test
+ public void testCanRequestMobileNetwork() throws Exception {
+ assertFalse(mUNM.mobileNetworkRequested());
+ assertEquals(0, mCM.requested.size());
+
+ mUNM.start();
+ assertFalse(mUNM.mobileNetworkRequested());
+ assertEquals(0, mCM.requested.size());
+
+ mUNM.mobileUpstreamRequiresDun(false);
+ assertFalse(mUNM.mobileNetworkRequested());
+ assertEquals(0, mCM.requested.size());
+
+ mUNM.registerMobileNetworkRequest();
+ assertTrue(mUNM.mobileNetworkRequested());
+ assertEquals(1, mCM.requested.size());
+ assertFalse(mCM.isDunRequested());
+
+ mUNM.stop();
+ assertFalse(mUNM.mobileNetworkRequested());
+ assertTrue(mCM.isEmpty());
+ }
+
+ @Test
+ public void testCanRequestDunNetwork() throws Exception {
+ assertFalse(mUNM.mobileNetworkRequested());
+ assertEquals(0, mCM.requested.size());
+
+ mUNM.start();
+ assertFalse(mUNM.mobileNetworkRequested());
+ assertEquals(0, mCM.requested.size());
+
+ mUNM.mobileUpstreamRequiresDun(true);
+ assertFalse(mUNM.mobileNetworkRequested());
+ assertEquals(0, mCM.requested.size());
+
+ mUNM.registerMobileNetworkRequest();
+ assertTrue(mUNM.mobileNetworkRequested());
+ assertEquals(1, mCM.requested.size());
+ assertTrue(mCM.isDunRequested());
+
+ mUNM.stop();
+ assertFalse(mUNM.mobileNetworkRequested());
+ assertTrue(mCM.isEmpty());
+ }
+
+ private static class TestConnectivityManager extends ConnectivityManager {
+ public Set<NetworkCallback> trackingDefault = new HashSet<>();
+ public Map<NetworkCallback, NetworkRequest> listening = new HashMap<>();
+ public Map<NetworkCallback, NetworkRequest> requested = new HashMap<>();
+
+ public TestConnectivityManager(Context ctx, IConnectivityManager svc) {
+ super(ctx, svc);
+ }
+
+ boolean isEmpty() {
+ return trackingDefault.isEmpty() &&
+ listening.isEmpty() &&
+ requested.isEmpty();
+ }
+
+ boolean isListeningForDun() {
+ for (NetworkRequest req : listening.values()) {
+ if (req.networkCapabilities.hasCapability(NET_CAPABILITY_DUN)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ boolean isDunRequested() {
+ for (NetworkRequest req : requested.values()) {
+ if (req.networkCapabilities.hasCapability(NET_CAPABILITY_DUN)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public void requestNetwork(NetworkRequest req, NetworkCallback cb) {
+ assertFalse(requested.containsKey(cb));
+ requested.put(cb, req);
+ }
+
+ @Override
+ public void registerNetworkCallback(NetworkRequest req, NetworkCallback cb) {
+ assertFalse(listening.containsKey(cb));
+ listening.put(cb, req);
+ }
+
+ @Override
+ public void registerDefaultNetworkCallback(NetworkCallback cb) {
+ assertFalse(trackingDefault.contains(cb));
+ trackingDefault.add(cb);
+ }
+
+ @Override
+ public void unregisterNetworkCallback(NetworkCallback cb) {
+ if (trackingDefault.contains(cb)) {
+ trackingDefault.remove(cb);
+ } else if (listening.containsKey(cb)) {
+ listening.remove(cb);
+ } else if (requested.containsKey(cb)) {
+ requested.remove(cb);
+ }
+
+ assertFalse(trackingDefault.contains(cb));
+ assertFalse(listening.containsKey(cb));
+ assertFalse(requested.containsKey(cb));
+ }
+ }
+}
diff --git a/tools/layoutlib/bridge/src/android/content/res/Resources_Delegate.java b/tools/layoutlib/bridge/src/android/content/res/Resources_Delegate.java
index c3d4cef6..e0f8e1c 100644
--- a/tools/layoutlib/bridge/src/android/content/res/Resources_Delegate.java
+++ b/tools/layoutlib/bridge/src/android/content/res/Resources_Delegate.java
@@ -21,6 +21,7 @@
import com.android.ide.common.rendering.api.DensityBasedResourceValue;
import com.android.ide.common.rendering.api.LayoutLog;
import com.android.ide.common.rendering.api.LayoutlibCallback;
+import com.android.ide.common.rendering.api.PluralsResourceValue;
import com.android.ide.common.rendering.api.RenderResources;
import com.android.ide.common.rendering.api.ResourceValue;
import com.android.layoutlib.bridge.Bridge;
@@ -43,6 +44,7 @@
import android.content.res.Resources.NotFoundException;
import android.content.res.Resources.Theme;
import android.graphics.drawable.Drawable;
+import android.icu.text.PluralRules;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.LruCache;
@@ -405,9 +407,6 @@
rv = resources.mContext.getRenderResources().resolveResValue(rv);
if (rv != null) {
return rv.getValue();
- } else {
- Bridge.getLog().error(LayoutLog.TAG_RESOURCES_RESOLVE,
- "Unable to resolve resource " + ref, null);
}
}
// Not a reference.
@@ -738,6 +737,48 @@
}
@LayoutlibDelegate
+ static String getQuantityString(Resources resources, int id, int quantity) throws
+ NotFoundException {
+ Pair<String, ResourceValue> value = getResourceValue(resources, id, mPlatformResourceFlag);
+
+ if (value != null) {
+ if (value.getSecond() instanceof PluralsResourceValue) {
+ PluralsResourceValue pluralsResourceValue = (PluralsResourceValue) value.getSecond();
+ PluralRules pluralRules = PluralRules.forLocale(resources.getConfiguration().getLocales()
+ .get(0));
+ String strValue = pluralsResourceValue.getValue(pluralRules.select(quantity));
+ if (strValue == null) {
+ strValue = pluralsResourceValue.getValue(PluralRules.KEYWORD_OTHER);
+ }
+
+ return strValue;
+ }
+ else {
+ return value.getSecond().getValue();
+ }
+ }
+
+ // id was not found or not resolved. Throw a NotFoundException.
+ throwException(resources, id);
+
+ // this is not used since the method above always throws
+ return null;
+ }
+
+ @LayoutlibDelegate
+ static String getQuantityString(Resources resources, int id, int quantity, Object... formatArgs)
+ throws NotFoundException {
+ String raw = getQuantityString(resources, id, quantity);
+ return String.format(resources.getConfiguration().getLocales().get(0), raw, formatArgs);
+ }
+
+ @LayoutlibDelegate
+ static CharSequence getQuantityText(Resources resources, int id, int quantity) throws
+ NotFoundException {
+ return getQuantityString(resources, id, quantity);
+ }
+
+ @LayoutlibDelegate
static void getValue(Resources resources, int id, TypedValue outValue, boolean resolveRefs)
throws NotFoundException {
Pair<String, ResourceValue> value = getResourceValue(resources, id, mPlatformResourceFlag);
diff --git a/tools/layoutlib/bridge/src/android/graphics/BidiRenderer.java b/tools/layoutlib/bridge/src/android/graphics/BidiRenderer.java
index 21f36ce..c6827a3 100644
--- a/tools/layoutlib/bridge/src/android/graphics/BidiRenderer.java
+++ b/tools/layoutlib/bridge/src/android/graphics/BidiRenderer.java
@@ -30,6 +30,7 @@
import java.awt.Toolkit;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
+import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.LinkedList;
@@ -41,6 +42,7 @@
*/
@SuppressWarnings("deprecation")
public class BidiRenderer {
+ private static String JAVA_VENDOR = System.getProperty("java.vendor");
private static class ScriptRun {
int start;
@@ -221,9 +223,16 @@
frc = mGraphics.getFontRenderContext();
} else {
frc = Toolkit.getDefaultToolkit().getFontMetrics(font).getFontRenderContext();
+
// Metrics obtained this way don't have anti-aliasing set. So,
// we create a new FontRenderContext with anti-aliasing set.
- frc = new FontRenderContext(font.getTransform(), mPaint.isAntiAliased(), frc.usesFractionalMetrics());
+ AffineTransform transform = font.getTransform();
+ if (mPaint.isAntiAliased() &&
+ // Workaround for http://b.android.com/211659
+ (transform.getScaleX() <= 9.9 ||
+ !"JetBrains s.r.o".equals(JAVA_VENDOR))) {
+ frc = new FontRenderContext(transform, true, frc.usesFractionalMetrics());
+ }
}
GlyphVector gv = font.layoutGlyphVector(frc, mText, start, limit, flag);
int ng = gv.getNumGlyphs();
diff --git a/tools/layoutlib/bridge/src/android/graphics/Canvas_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/Canvas_Delegate.java
index 43a0ff5..c599e9d 100644
--- a/tools/layoutlib/bridge/src/android/graphics/Canvas_Delegate.java
+++ b/tools/layoutlib/bridge/src/android/graphics/Canvas_Delegate.java
@@ -20,24 +20,14 @@
import com.android.layoutlib.bridge.Bridge;
import com.android.layoutlib.bridge.impl.DelegateManager;
import com.android.layoutlib.bridge.impl.GcSnapshot;
-import com.android.layoutlib.bridge.impl.PorterDuffUtility;
-import com.android.ninepatch.NinePatchChunk;
import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
import android.annotation.Nullable;
import android.graphics.Bitmap.Config;
-import android.text.TextUtils;
-import java.awt.Color;
-import java.awt.Composite;
import java.awt.Graphics2D;
import java.awt.Rectangle;
-import java.awt.RenderingHints;
-import java.awt.Shape;
import java.awt.geom.AffineTransform;
-import java.awt.geom.Arc2D;
-import java.awt.geom.Rectangle2D;
-import java.awt.image.BufferedImage;
import libcore.util.NativeAllocationRegistry_Delegate;
@@ -401,23 +391,6 @@
}
@LayoutlibDelegate
- public static boolean nClipRegion(long nativeCanvas,
- long nativeRegion,
- int regionOp) {
- Canvas_Delegate canvasDelegate = Canvas_Delegate.getDelegate(nativeCanvas);
- if (canvasDelegate == null) {
- return true;
- }
-
- Region_Delegate region = Region_Delegate.getDelegate(nativeRegion);
- if (region == null) {
- return true;
- }
-
- return canvasDelegate.mSnapshot.clip(region.getJavaArea(), regionOp);
- }
-
- @LayoutlibDelegate
public static void nSetDrawFilter(long nativeCanvas, long nativeFilter) {
Canvas_Delegate canvasDelegate = Canvas_Delegate.getDelegate(nativeCanvas);
if (canvasDelegate == null) {
diff --git a/tools/layoutlib/bridge/src/android/view/PointerIcon_Delegate.java b/tools/layoutlib/bridge/src/android/view/PointerIcon_Delegate.java
new file mode 100644
index 0000000..4a5ea9b
--- /dev/null
+++ b/tools/layoutlib/bridge/src/android/view/PointerIcon_Delegate.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+import android.content.Context;
+import android.content.res.Resources;
+
+public class PointerIcon_Delegate {
+
+ @LayoutlibDelegate
+ /*package*/ static void loadResource(PointerIcon icon, Context context, Resources resources,
+ int resourceId) {
+ // HACK: This bypasses the problem of having an enum resolved as a resourceId.
+ // PointerIcon would not be displayed by layoutlib anyway, so we always return the null
+ // icon.
+ }
+}
diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java
index 1b3b563..663e56d 100644
--- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java
+++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java
@@ -66,7 +66,6 @@
import android.graphics.drawable.Drawable;
import android.hardware.display.DisplayManager;
import android.net.Uri;
-import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
@@ -104,6 +103,7 @@
import java.util.List;
import java.util.Map;
+import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1;
import static com.android.layoutlib.bridge.android.RenderParamsFlags.FLAG_KEY_APPLICATION_PACKAGE;
/**
@@ -113,6 +113,28 @@
public final class BridgeContext extends Context {
private static final String PREFIX_THEME_APPCOMPAT = "Theme.AppCompat";
+ private static final Map<String, ResourceValue> FRAMEWORK_PATCHED_VALUES = new HashMap<>(2);
+ private static final Map<String, ResourceValue> FRAMEWORK_REPLACE_VALUES = new HashMap<>(3);
+
+ static {
+ FRAMEWORK_PATCHED_VALUES.put("animateFirstView", new ResourceValue(
+ ResourceType.BOOL, "animateFirstView", "false", false));
+ FRAMEWORK_PATCHED_VALUES.put("animateLayoutChanges",
+ new ResourceValue(ResourceType.BOOL, "animateLayoutChanges", "false", false));
+
+
+ FRAMEWORK_REPLACE_VALUES.put("textEditSuggestionItemLayout",
+ new ResourceValue(ResourceType.LAYOUT, "textEditSuggestionItemLayout",
+ "text_edit_suggestion_item", true));
+ FRAMEWORK_REPLACE_VALUES.put("textEditSuggestionContainerLayout",
+ new ResourceValue(ResourceType.LAYOUT, "textEditSuggestionContainerLayout",
+ "text_edit_suggestion_container", true));
+ FRAMEWORK_REPLACE_VALUES.put("textEditSuggestionHighlightStyle",
+ new ResourceValue(ResourceType.STYLE, "textEditSuggestionHighlightStyle",
+ "TextAppearance.Holo.SuggestionHighlight", true));
+
+ }
+
/** The map adds cookies to each view so that IDE can link xml tags to views. */
private final HashMap<View, Object> mViewKeyMap = new HashMap<>();
/**
@@ -312,7 +334,7 @@
* Returns the current parser at the top the of the stack.
* @return a parser or null.
*/
- public BridgeXmlBlockParser getCurrentParser() {
+ private BridgeXmlBlockParser getCurrentParser() {
return mParserStack.peek();
}
@@ -406,7 +428,8 @@
}
public Pair<View, Boolean> inflateView(ResourceReference resource, ViewGroup parent,
- boolean attachToRoot, boolean skipCallbackParser) {
+ @SuppressWarnings("SameParameterValue") boolean attachToRoot,
+ boolean skipCallbackParser) {
boolean isPlatformLayout = resource.isFramework();
if (!isPlatformLayout && !skipCallbackParser) {
@@ -711,11 +734,7 @@
Object key = parser.getViewCookie();
if (key != null) {
- defaultPropMap = mDefaultPropMaps.get(key);
- if (defaultPropMap == null) {
- defaultPropMap = new PropertiesMap();
- mDefaultPropMaps.put(key, defaultPropMap);
- }
+ defaultPropMap = mDefaultPropMaps.computeIfAbsent(key, k -> new PropertiesMap());
}
} else if (set instanceof BridgeLayoutParamsMapAttributes) {
@@ -909,6 +928,16 @@
// if there's no direct value for this attribute in the XML, we look for default
// values in the widget defStyle, and then in the theme.
if (value == null) {
+ if (frameworkAttr) {
+ // For some framework values, layoutlib patches the actual value in the
+ // theme when it helps to improve the final preview. In most cases
+ // we just disable animations.
+ ResourceValue patchedValue = FRAMEWORK_PATCHED_VALUES.get(attrName);
+ if (patchedValue != null) {
+ defaultValue = patchedValue;
+ }
+ }
+
// if we found a value, we make sure this doesn't reference another value.
// So we resolve it.
if (defaultValue != null) {
@@ -916,16 +945,21 @@
// exist, we should log a warning and omit it.
String val = defaultValue.getValue();
if (val != null && val.startsWith(SdkConstants.PREFIX_THEME_REF)) {
- if (!attrName.equals(RTL_ATTRS.get(val)) ||
- getApplicationInfo().targetSdkVersion <
- VERSION_CODES.JELLY_BEAN_MR1) {
+ // Because we always use the latest framework code, some resources might
+ // fail to resolve when using old themes (they haven't been backported).
+ // Since this is an artifact caused by us using always the latest
+ // code, we check for some of those values and replace them here.
+ defaultValue = FRAMEWORK_REPLACE_VALUES.get(attrName);
+
+ if (defaultValue == null &&
+ (getApplicationInfo().targetSdkVersion < JELLY_BEAN_MR1 ||
+ !attrName.equals(RTL_ATTRS.get(val)))) {
// Only log a warning if the referenced value isn't one of the RTL
// attributes, or the app targets old API.
Bridge.getLog().warning(LayoutLog.TAG_RESOURCES_RESOLVE_THEME_ATTR,
String.format("Failed to find '%s' in current theme.", val),
val);
}
- defaultValue = null;
}
}
@@ -1944,7 +1978,7 @@
Map<List<StyleResourceValue>,
Map<Integer, Pair<BridgeTypedArray, PropertiesMap>>>> mCache;
- public TypedArrayCache() {
+ private TypedArrayCache() {
mCache = new IdentityHashMap<>();
}
@@ -1965,17 +1999,9 @@
public void put(int[] attrs, List<StyleResourceValue> themes, int resId,
Pair<BridgeTypedArray, PropertiesMap> value) {
Map<List<StyleResourceValue>, Map<Integer, Pair<BridgeTypedArray, PropertiesMap>>>
- cacheFromThemes = mCache.get(attrs);
- if (cacheFromThemes == null) {
- cacheFromThemes = new HashMap<>();
- mCache.put(attrs, cacheFromThemes);
- }
+ cacheFromThemes = mCache.computeIfAbsent(attrs, k -> new HashMap<>());
Map<Integer, Pair<BridgeTypedArray, PropertiesMap>> cacheFromResId =
- cacheFromThemes.get(themes);
- if (cacheFromResId == null) {
- cacheFromResId = new HashMap<>();
- cacheFromThemes.put(themes, cacheFromResId);
- }
+ cacheFromThemes.computeIfAbsent(themes, k -> new HashMap<>());
cacheFromResId.put(resId, value);
}
diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/Config.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/Config.java
index 09937bc..5386b17 100644
--- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/Config.java
+++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/Config.java
@@ -74,8 +74,8 @@
}
public static String getTime(int platformVersion) {
- if (isGreaterOrEqual(platformVersion, M)) {
- return "6:00";
+ if (isGreaterOrEqual(platformVersion, N)) {
+ return "7:00";
}
if (platformVersion < GINGERBREAD) {
return "2:20";
@@ -98,6 +98,9 @@
if (platformVersion < M) {
return "5:10";
}
+ if (platformVersion < N) {
+ return "6:00";
+ }
// Should never happen.
return "4:04";
}
diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderSessionImpl.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderSessionImpl.java
index 14f783b..85fe2a4 100644
--- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderSessionImpl.java
+++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderSessionImpl.java
@@ -385,13 +385,10 @@
}
/**
- * Renders the given view hierarchy to the passed canvas and returns the result of the render
- * operation.
- * @param canvas an optional canvas to render the views to. If null, only the measure and
- * layout steps will be executed.
+ * Runs a layout pass for the given view root
*/
- private static Result renderAndBuildResult(@NonNull BridgeContext context, @NonNull ViewGroup viewRoot,
- @Nullable Canvas canvas, int width, int height) {
+ private static void doLayout(@NonNull BridgeContext context, @NonNull ViewGroup viewRoot,
+ int width, int height) {
// measure again with the size we need
// This must always be done before the call to layout
measureView(viewRoot, null /*measuredView*/,
@@ -401,7 +398,16 @@
// now do the layout.
viewRoot.layout(0, 0, width, height);
handleScrolling(context, viewRoot);
+ }
+ /**
+ * Renders the given view hierarchy to the passed canvas and returns the result of the render
+ * operation.
+ * @param canvas an optional canvas to render the views to. If null, only the measure and
+ * layout steps will be executed.
+ */
+ private static Result renderAndBuildResult(@NonNull BridgeContext context, @NonNull ViewGroup viewRoot,
+ @Nullable Canvas canvas, int width, int height) {
if (canvas == null) {
return SUCCESS.createResult();
}
@@ -479,6 +485,7 @@
// delete the canvas and image to reset them on the next full rendering
mImage = null;
mCanvas = null;
+ doLayout(getContext(), mViewRoot, mMeasuredScreenWidth, mMeasuredScreenHeight);
} else {
// draw the views
// create the BufferedImage into which the layout will be rendered.
@@ -539,6 +546,7 @@
gc.dispose();
}
+ doLayout(getContext(), mViewRoot, mMeasuredScreenWidth, mMeasuredScreenHeight);
if (mElapsedFrameTimeNanos >= 0) {
long initialTime = System_Delegate.nanoTime();
if (!mFirstFrameExecuted) {
diff --git a/tools/layoutlib/bridge/src/libcore/util/NativeAllocationRegistry_Delegate.java b/tools/layoutlib/bridge/src/libcore/util/NativeAllocationRegistry_Delegate.java
index 6246ec1b8..04fabc2 100644
--- a/tools/layoutlib/bridge/src/libcore/util/NativeAllocationRegistry_Delegate.java
+++ b/tools/layoutlib/bridge/src/libcore/util/NativeAllocationRegistry_Delegate.java
@@ -52,9 +52,15 @@
@LayoutlibDelegate
/*package*/ static void applyFreeFunction(long freeFunction, long nativePtr) {
- NativeAllocationRegistry_Delegate delegate = sManager.getDelegate(freeFunction);
- if (delegate != null) {
- delegate.mFinalizer.free(nativePtr);
+ // This method MIGHT run in the context of the finalizer thread. If the delegate method
+ // crashes, it could bring down the VM. That's why we catch all the exceptions and ignore
+ // them.
+ try {
+ NativeAllocationRegistry_Delegate delegate = sManager.getDelegate(freeFunction);
+ if (delegate != null) {
+ delegate.mFinalizer.free(nativePtr);
+ }
+ } catch (Throwable ignore) {
}
}
diff --git a/tools/layoutlib/bridge/tests/Android.mk b/tools/layoutlib/bridge/tests/Android.mk
index 9ee416a..33d55de 100644
--- a/tools/layoutlib/bridge/tests/Android.mk
+++ b/tools/layoutlib/bridge/tests/Android.mk
@@ -30,7 +30,8 @@
layoutlib_api-prebuilt \
tools-common-prebuilt \
sdk-common \
- junit-host
+ junit-host \
+ guavalib
include $(BUILD_HOST_JAVA_LIBRARY)
diff --git a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/activity.png b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/activity.png
index f274dbf..199ea60 100644
--- a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/activity.png
+++ b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/activity.png
Binary files differ
diff --git a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/allwidgets.png b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/allwidgets.png
index ef95f83..89ff5db 100644
--- a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/allwidgets.png
+++ b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/allwidgets.png
Binary files differ
diff --git a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/allwidgets_tab.png b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/allwidgets_tab.png
index 6eeb82c..1f4405d 100644
--- a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/allwidgets_tab.png
+++ b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/allwidgets_tab.png
Binary files differ
diff --git a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/array_check.png b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/array_check.png
index 336f9d8..c3bd708 100644
--- a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/array_check.png
+++ b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/array_check.png
Binary files differ
diff --git a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/simple_activity-old-theme.png b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/simple_activity-old-theme.png
new file mode 100644
index 0000000..eb431b0
--- /dev/null
+++ b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/simple_activity-old-theme.png
Binary files differ
diff --git a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/simple_activity_noactionbar.png b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/simple_activity_noactionbar.png
index 290018b..b756719 100644
--- a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/simple_activity_noactionbar.png
+++ b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/simple_activity_noactionbar.png
Binary files differ
diff --git a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/layout/allwidgets.xml b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/layout/allwidgets.xml
index adb58a3..05a3665 100644
--- a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/layout/allwidgets.xml
+++ b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/layout/allwidgets.xml
@@ -28,7 +28,8 @@
android:layout_alignParentStart="true"
android:layout_below="@id/frameLayout"
android:text="Large Text"
- android:textAppearance="?android:attr/textAppearanceLarge" />
+ android:textAppearance="?android:attr/textAppearanceLarge"
+ android:pointerIcon="hand" />
<TextView
android:id="@id/textView3"
diff --git a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/values/arrays.xml b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/values/arrays.xml
index f6e14d2..5f58d39 100644
--- a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/values/arrays.xml
+++ b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/values/arrays.xml
@@ -17,6 +17,7 @@
<!-- theme ref in android NS. value = @string/candidates_style = <u>candidates</u> -->
<item>?android:attr/candidatesTextStyleSpans</item>
<item>@android:string/unknownName</item> <!-- value = Unknown -->
+ <item>?EC</item>
</string-array>
<!-- resources that the above array can refer to -->
diff --git a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/values/styles.xml b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/values/styles.xml
index c8a5fec..debe33b 100644
--- a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/values/styles.xml
+++ b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/values/styles.xml
@@ -3,7 +3,6 @@
<!-- Base application theme. -->
<style name="AppTheme" parent="android:Theme.Material.Light.DarkActionBar">
<item name="myattr">@integer/ten</item>
- <item name="android:animateFirstView">false</item>
</style>
</resources>
diff --git a/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/Main.java b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/Main.java
index cdcae89..ded52a7 100644
--- a/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/Main.java
+++ b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/Main.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2014 The Android Open Source Project
+ * Copyright (C) 2016 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.
@@ -16,815 +16,23 @@
package com.android.layoutlib.bridge.intensive;
-import com.android.ide.common.rendering.api.LayoutLog;
-import com.android.ide.common.rendering.api.RenderSession;
-import com.android.ide.common.rendering.api.Result;
-import com.android.ide.common.rendering.api.SessionParams;
-import com.android.ide.common.rendering.api.SessionParams.RenderingMode;
-import com.android.ide.common.rendering.api.ViewInfo;
-import com.android.ide.common.resources.FrameworkResources;
-import com.android.ide.common.resources.ResourceItem;
-import com.android.ide.common.resources.ResourceRepository;
-import com.android.ide.common.resources.ResourceResolver;
-import com.android.ide.common.resources.configuration.FolderConfiguration;
-import com.android.io.FolderWrapper;
-import com.android.layoutlib.bridge.Bridge;
-import com.android.layoutlib.bridge.android.BridgeContext;
-import com.android.layoutlib.bridge.android.RenderParamsFlags;
-import com.android.layoutlib.bridge.impl.DelegateManager;
-import com.android.layoutlib.bridge.impl.RenderAction;
-import com.android.layoutlib.bridge.intensive.setup.ConfigGenerator;
-import com.android.layoutlib.bridge.intensive.setup.LayoutLibTestCallback;
-import com.android.layoutlib.bridge.intensive.setup.LayoutPullParser;
-import com.android.resources.Density;
-import com.android.resources.Navigation;
-import com.android.resources.ResourceType;
-import com.android.resources.ScreenOrientation;
-import com.android.tools.layoutlib.java.System_Delegate;
-import com.android.utils.ILogger;
+import com.android.layoutlib.bridge.TestDelegates;
+import com.android.layoutlib.bridge.android.BridgeXmlBlockParserTest;
+import com.android.layoutlib.bridge.impl.LayoutParserWrapperTest;
-import org.junit.AfterClass;
-import org.junit.Before;
-import org.junit.BeforeClass;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TestWatcher;
-import org.junit.runner.Description;
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+import org.junit.runners.Suite.SuiteClasses;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.content.res.AssetManager;
-import android.content.res.Configuration;
-import android.content.res.Resources;
-import android.util.DisplayMetrics;
-
-import java.io.File;
-import java.io.IOException;
-import java.lang.ref.WeakReference;
-import java.lang.reflect.Field;
-import java.net.URL;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.concurrent.TimeUnit;
-
-import com.google.android.collect.Lists;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
+import android.graphics.Matrix_DelegateTest;
/**
- * This is a set of tests that loads all the framework resources and a project checked in this
- * test's resources. The main dependencies
- * are:
- * 1. Fonts directory.
- * 2. Framework Resources.
- * 3. App resources.
- * 4. build.prop file
- *
- * These are configured by two variables set in the system properties.
- *
- * 1. platform.dir: This is the directory for the current platform in the built SDK
- * (.../sdk/platforms/android-<version>).
- *
- * The fonts are platform.dir/data/fonts.
- * The Framework resources are platform.dir/data/res.
- * build.prop is at platform.dir/build.prop.
- *
- * 2. test_res.dir: This is the directory for the resources of the test. If not specified, this
- * falls back to getClass().getProtectionDomain().getCodeSource().getLocation()
- *
- * The app resources are at: test_res.dir/testApp/MyApplication/app/src/main/res
+ * Suite used by the layoutlib build system
*/
+@RunWith(Suite.class)
+@SuiteClasses({
+ RenderTests.class, LayoutParserWrapperTest.class, BridgeXmlBlockParserTest.class,
+ Matrix_DelegateTest.class, TestDelegates.class
+})
public class Main {
-
- private static final String PLATFORM_DIR_PROPERTY = "platform.dir";
- private static final String RESOURCE_DIR_PROPERTY = "test_res.dir";
-
- private static final String PLATFORM_DIR;
- private static final String TEST_RES_DIR;
- /** Location of the app to test inside {@link #TEST_RES_DIR}*/
- private static final String APP_TEST_DIR = "testApp/MyApplication";
- /** Location of the app's res dir inside {@link #TEST_RES_DIR}*/
- private static final String APP_TEST_RES = APP_TEST_DIR + "/src/main/res";
- private static final String APP_CLASSES_LOCATION =
- APP_TEST_DIR + "/build/intermediates/classes/debug/";
-
- private static LayoutLog sLayoutLibLog;
- private static FrameworkResources sFrameworkRepo;
- private static ResourceRepository sProjectResources;
- private static ILogger sLogger;
- private static Bridge sBridge;
-
- /** List of log messages generated by a render call. It can be used to find specific errors */
- private static ArrayList<String> sRenderMessages = Lists.newArrayList();
-
- // Default class loader with access to the app classes
- private ClassLoader mDefaultClassLoader =
- new ModuleClassLoader(APP_CLASSES_LOCATION, getClass().getClassLoader());
-
- @Rule
- public TestWatcher sRenderMessageWatcher = new TestWatcher() {
- @Override
- protected void succeeded(Description description) {
- // We only check error messages if the rest of the test case was successful.
- if (!sRenderMessages.isEmpty()) {
- fail(description.getMethodName() + " render error message: " + sRenderMessages.get
- (0));
- }
- }
- };
-
- static {
- // Test that System Properties are properly set.
- PLATFORM_DIR = getPlatformDir();
- if (PLATFORM_DIR == null) {
- fail(String.format("System Property %1$s not properly set. The value is %2$s",
- PLATFORM_DIR_PROPERTY, System.getProperty(PLATFORM_DIR_PROPERTY)));
- }
-
- TEST_RES_DIR = getTestResDir();
- if (TEST_RES_DIR == null) {
- fail(String.format("System property %1$s.dir not properly set. The value is %2$s",
- RESOURCE_DIR_PROPERTY, System.getProperty(RESOURCE_DIR_PROPERTY)));
- }
- }
-
- private static String getPlatformDir() {
- String platformDir = System.getProperty(PLATFORM_DIR_PROPERTY);
- if (platformDir != null && !platformDir.isEmpty() && new File(platformDir).isDirectory()) {
- return platformDir;
- }
- // System Property not set. Try to find the directory in the build directory.
- String androidHostOut = System.getenv("ANDROID_HOST_OUT");
- if (androidHostOut != null) {
- platformDir = getPlatformDirFromHostOut(new File(androidHostOut));
- if (platformDir != null) {
- return platformDir;
- }
- }
- String workingDirString = System.getProperty("user.dir");
- File workingDir = new File(workingDirString);
- // Test if workingDir is android checkout root.
- platformDir = getPlatformDirFromRoot(workingDir);
- if (platformDir != null) {
- return platformDir;
- }
-
- // Test if workingDir is platform/frameworks/base/tools/layoutlib/bridge.
- File currentDir = workingDir;
- if (currentDir.getName().equalsIgnoreCase("bridge")) {
- currentDir = currentDir.getParentFile();
- }
- // Test if currentDir is platform/frameworks/base/tools/layoutlib. That is, root should be
- // workingDir/../../../../ (4 levels up)
- for (int i = 0; i < 4; i++) {
- if (currentDir != null) {
- currentDir = currentDir.getParentFile();
- }
- }
- return currentDir == null ? null : getPlatformDirFromRoot(currentDir);
- }
-
- private static String getPlatformDirFromRoot(File root) {
- if (!root.isDirectory()) {
- return null;
- }
- File out = new File(root, "out");
- if (!out.isDirectory()) {
- return null;
- }
- File host = new File(out, "host");
- if (!host.isDirectory()) {
- return null;
- }
- File[] hosts = host.listFiles(path -> path.isDirectory() &&
- (path.getName().startsWith("linux-") || path.getName().startsWith("darwin-")));
- assert hosts != null;
- for (File hostOut : hosts) {
- String platformDir = getPlatformDirFromHostOut(hostOut);
- if (platformDir != null) {
- return platformDir;
- }
- }
- return null;
- }
-
- private static String getPlatformDirFromHostOut(File out) {
- if (!out.isDirectory()) {
- return null;
- }
- File sdkDir = new File(out, "sdk");
- if (!sdkDir.isDirectory()) {
- return null;
- }
- File[] sdkDirs = sdkDir.listFiles(path -> {
- // We need to search for $TARGET_PRODUCT (usually, sdk_phone_armv7)
- return path.isDirectory() && path.getName().startsWith("sdk");
- });
- assert sdkDirs != null;
- for (File dir : sdkDirs) {
- String platformDir = getPlatformDirFromHostOutSdkSdk(dir);
- if (platformDir != null) {
- return platformDir;
- }
- }
- return null;
- }
-
- private static String getPlatformDirFromHostOutSdkSdk(File sdkDir) {
- File[] possibleSdks = sdkDir.listFiles(
- path -> path.isDirectory() && path.getName().contains("android-sdk"));
- assert possibleSdks != null;
- for (File possibleSdk : possibleSdks) {
- File platformsDir = new File(possibleSdk, "platforms");
- File[] platforms = platformsDir.listFiles(
- path -> path.isDirectory() && path.getName().startsWith("android-"));
- if (platforms == null || platforms.length == 0) {
- continue;
- }
- Arrays.sort(platforms, (o1, o2) -> {
- final int MAX_VALUE = 1000;
- String suffix1 = o1.getName().substring("android-".length());
- String suffix2 = o2.getName().substring("android-".length());
- int suff1, suff2;
- try {
- suff1 = Integer.parseInt(suffix1);
- } catch (NumberFormatException e) {
- suff1 = MAX_VALUE;
- }
- try {
- suff2 = Integer.parseInt(suffix2);
- } catch (NumberFormatException e) {
- suff2 = MAX_VALUE;
- }
- if (suff1 != MAX_VALUE || suff2 != MAX_VALUE) {
- return suff2 - suff1;
- }
- return suffix2.compareTo(suffix1);
- });
- return platforms[0].getAbsolutePath();
- }
- return null;
- }
-
- private static String getTestResDir() {
- String resourceDir = System.getProperty(RESOURCE_DIR_PROPERTY);
- if (resourceDir != null && !resourceDir.isEmpty() && new File(resourceDir).isDirectory()) {
- return resourceDir;
- }
- // TEST_RES_DIR not explicitly set. Fallback to the class's source location.
- try {
- URL location = Main.class.getProtectionDomain().getCodeSource().getLocation();
- return new File(location.getPath()).exists() ? location.getPath() : null;
- } catch (NullPointerException e) {
- // Prevent a lot of null checks by just catching the exception.
- return null;
- }
- }
-
- /**
- * Initialize the bridge and the resource maps.
- */
- @BeforeClass
- public static void setUp() {
- File data_dir = new File(PLATFORM_DIR, "data");
- File res = new File(data_dir, "res");
- sFrameworkRepo = new FrameworkResources(new FolderWrapper(res));
- sFrameworkRepo.loadResources();
- sFrameworkRepo.loadPublicResources(getLogger());
-
- sProjectResources =
- new ResourceRepository(new FolderWrapper(TEST_RES_DIR + "/" + APP_TEST_RES),
- false) {
- @NonNull
- @Override
- protected ResourceItem createResourceItem(@NonNull String name) {
- return new ResourceItem(name);
- }
- };
- sProjectResources.loadResources();
-
- File fontLocation = new File(data_dir, "fonts");
- File buildProp = new File(PLATFORM_DIR, "build.prop");
- File attrs = new File(res, "values" + File.separator + "attrs.xml");
- sBridge = new Bridge();
- sBridge.init(ConfigGenerator.loadProperties(buildProp), fontLocation,
- ConfigGenerator.getEnumMap(attrs), getLayoutLog());
- }
-
- @Before
- public void beforeTestCase() {
- sRenderMessages.clear();
- }
-
- /** Test activity.xml */
- @Test
- public void testActivity() throws ClassNotFoundException {
- renderAndVerify("activity.xml", "activity.png");
- }
-
- @Test
- public void testTranslucentBars() throws ClassNotFoundException {
- LayoutLibTestCallback layoutLibCallback =
- new LayoutLibTestCallback(getLogger(), mDefaultClassLoader);
- layoutLibCallback.initResources();
-
- LayoutPullParser parser = createLayoutPullParser("four_corners.xml");
- SessionParams params = getSessionParams(parser, ConfigGenerator.NEXUS_5,
- layoutLibCallback, "Theme.Material.Light.NoActionBar.TranslucentDecor", false,
- RenderingMode.NORMAL, 22);
- renderAndVerify(params, "four_corners_translucent.png");
-
- parser = createLayoutPullParser("four_corners.xml");
- params = getSessionParams(parser, ConfigGenerator.NEXUS_5_LAND,
- layoutLibCallback, "Theme.Material.Light.NoActionBar.TranslucentDecor", false,
- RenderingMode.NORMAL, 22);
- renderAndVerify(params, "four_corners_translucent_land.png");
-
- parser = createLayoutPullParser("four_corners.xml");
- params = getSessionParams(parser, ConfigGenerator.NEXUS_5,
- layoutLibCallback, "Theme.Material.Light.NoActionBar", false,
- RenderingMode.NORMAL, 22);
- renderAndVerify(params, "four_corners.png");
- }
-
- private static void gc() {
- // See RuntimeUtil#gc in jlibs (http://jlibs.in/)
- Object obj = new Object();
- WeakReference ref = new WeakReference<>(obj);
- //noinspection UnusedAssignment
- obj = null;
- while(ref.get() != null) {
- System.gc();
- System.runFinalization();
- }
-
- System.gc();
- System.runFinalization();
- }
-
- /** Test allwidgets.xml */
- @Test
- public void testAllWidgets() throws ClassNotFoundException {
- renderAndVerify("allwidgets.xml", "allwidgets.png");
-
- // We expect fidelity warnings for Path.isConvex. Fail for anything else.
- sRenderMessages.removeIf(message -> message.equals("Path.isConvex is not supported."));
- }
-
- @Test
- public void testArrayCheck() throws ClassNotFoundException {
- renderAndVerify("array_check.xml", "array_check.png");
- }
-
- @Test
- public void testAllWidgetsTablet() throws ClassNotFoundException {
- renderAndVerify("allwidgets.xml", "allwidgets_tab.png", ConfigGenerator.NEXUS_7_2012);
-
- // We expect fidelity warnings for Path.isConvex. Fail for anything else.
- sRenderMessages.removeIf(message -> message.equals("Path.isConvex is not supported."));
- }
-
- @Test
- public void testActivityActionBar() throws ClassNotFoundException {
- LayoutPullParser parser = createLayoutPullParser("simple_activity.xml");
- LayoutLibTestCallback layoutLibCallback =
- new LayoutLibTestCallback(getLogger(), mDefaultClassLoader);
- layoutLibCallback.initResources();
-
- SessionParams params = getSessionParams(parser, ConfigGenerator.NEXUS_5,
- layoutLibCallback, "Theme.Material.Light.NoActionBar", false,
- RenderingMode.V_SCROLL, 22);
-
- renderAndVerify(params, "simple_activity_noactionbar.png");
-
- parser = createLayoutPullParser("simple_activity.xml");
- params = getSessionParams(parser, ConfigGenerator.NEXUS_5,
- layoutLibCallback, "Theme.Material.Light", false,
- RenderingMode.V_SCROLL, 22);
-
- renderAndVerify(params, "simple_activity.png");
-
- // This also tests that a theme with "NoActionBar" DOES HAVE an action bar when we are
- // displaying menus.
- parser = createLayoutPullParser("simple_activity.xml");
- params = getSessionParams(parser, ConfigGenerator.NEXUS_5,
- layoutLibCallback, "Theme.Material.Light.NoActionBar", false,
- RenderingMode.V_SCROLL, 22);
- params.setFlag(RenderParamsFlags.FLAG_KEY_ROOT_TAG, "menu");
- renderAndVerify(params, "simple_activity.png");
- }
-
- @Test
- public void testOnApplyInsetsCall()
- throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
- // We get the widget via reflection to avoid IntelliJ complaining about the class being
- // located in the wrong package. (From the Bridge tests point of view, it is)
- Class insetsWidgetClass = Class.forName("com.android.layoutlib.test.myapplication.widgets" +
- ".InsetsWidget");
- Field field = insetsWidgetClass.getDeclaredField("sApplyInsetsCalled");
- assertFalse((Boolean)field.get(null));
-
- LayoutPullParser parser = createLayoutPullParser("insets.xml");
- LayoutLibTestCallback layoutLibCallback =
- new LayoutLibTestCallback(getLogger(), mDefaultClassLoader);
- layoutLibCallback.initResources();
- SessionParams params = getSessionParams(parser, ConfigGenerator.NEXUS_5,
- layoutLibCallback, "Theme.Material.Light.NoActionBar", false,
- RenderingMode.NORMAL, 22);
-
- render(params, -1);
-
- assertTrue((Boolean)field.get(null));
- field.set(null, false);
- }
-
- @AfterClass
- public static void tearDown() {
- sLayoutLibLog = null;
- sFrameworkRepo = null;
- sProjectResources = null;
- sLogger = null;
- sBridge = null;
-
- gc();
-
- System.out.println("Objects still linked from the DelegateManager:");
- DelegateManager.dump(System.out);
- }
-
- /** Test expand_layout.xml */
- @Test
- public void testExpand() throws ClassNotFoundException {
- // Create the layout pull parser.
- LayoutPullParser parser = createLayoutPullParser("expand_vert_layout.xml");
- // Create LayoutLibCallback.
- LayoutLibTestCallback layoutLibCallback =
- new LayoutLibTestCallback(getLogger(), mDefaultClassLoader);
- layoutLibCallback.initResources();
-
- ConfigGenerator customConfigGenerator = new ConfigGenerator()
- .setScreenWidth(300)
- .setScreenHeight(20)
- .setDensity(Density.XHIGH)
- .setNavigation(Navigation.NONAV);
-
- SessionParams params = getSessionParams(parser, customConfigGenerator,
- layoutLibCallback, "Theme.Material.Light.NoActionBar.Fullscreen", false,
- RenderingMode.V_SCROLL, 22);
-
- renderAndVerify(params, "expand_vert_layout.png");
-
- customConfigGenerator = new ConfigGenerator()
- .setScreenWidth(20)
- .setScreenHeight(300)
- .setDensity(Density.XHIGH)
- .setNavigation(Navigation.NONAV);
- parser = createLayoutPullParser("expand_horz_layout.xml");
- params = getSessionParams(parser, customConfigGenerator,
- layoutLibCallback, "Theme.Material.Light.NoActionBar.Fullscreen", false,
- RenderingMode.H_SCROLL, 22);
-
- renderAndVerify(params, "expand_horz_layout.png");
- }
-
- /** Test indeterminate_progressbar.xml */
- @Test
- public void testVectorAnimation() throws ClassNotFoundException {
- // Create the layout pull parser.
- LayoutPullParser parser = createLayoutPullParser("indeterminate_progressbar.xml");
- // Create LayoutLibCallback.
- LayoutLibTestCallback layoutLibCallback =
- new LayoutLibTestCallback(getLogger(), mDefaultClassLoader);
- layoutLibCallback.initResources();
-
- SessionParams params = getSessionParams(parser, ConfigGenerator.NEXUS_5,
- layoutLibCallback, "Theme.Material.NoActionBar.Fullscreen", false,
- RenderingMode.V_SCROLL, 22);
-
- renderAndVerify(params, "animated_vector.png", TimeUnit.SECONDS.toNanos(2));
-
- parser = createLayoutPullParser("indeterminate_progressbar.xml");
- params = getSessionParams(parser, ConfigGenerator.NEXUS_5,
- layoutLibCallback, "Theme.Material.NoActionBar.Fullscreen", false,
- RenderingMode.V_SCROLL, 22);
- renderAndVerify(params, "animated_vector_1.png", TimeUnit.SECONDS.toNanos(3));
- }
-
- /**
- * Test a vector drawable that uses trimStart and trimEnd. It also tests all the primitives
- * for vector drawables (lines, moves and cubic and quadratic curves).
- */
- @Test
- public void testVectorDrawable() throws ClassNotFoundException {
- // Create the layout pull parser.
- LayoutPullParser parser = createLayoutPullParser("vector_drawable.xml");
- // Create LayoutLibCallback.
- LayoutLibTestCallback layoutLibCallback =
- new LayoutLibTestCallback(getLogger(), mDefaultClassLoader);
- layoutLibCallback.initResources();
-
- SessionParams params = getSessionParams(parser, ConfigGenerator.NEXUS_5,
- layoutLibCallback, "Theme.Material.NoActionBar.Fullscreen", false,
- RenderingMode.V_SCROLL, 22);
-
- renderAndVerify(params, "vector_drawable.png", TimeUnit.SECONDS.toNanos(2));
- }
-
- /**
- * Regression test for http://b.android.com/91383 and http://b.android.com/203797
- */
- @Test
- public void testVectorDrawable91383() throws ClassNotFoundException {
- // Create the layout pull parser.
- LayoutPullParser parser = createLayoutPullParser("vector_drawable_android.xml");
- // Create LayoutLibCallback.
- LayoutLibTestCallback layoutLibCallback =
- new LayoutLibTestCallback(getLogger(), mDefaultClassLoader);
- layoutLibCallback.initResources();
-
- SessionParams params = getSessionParams(parser, ConfigGenerator.NEXUS_5,
- layoutLibCallback, "Theme.Material.NoActionBar.Fullscreen", false,
- RenderingMode.V_SCROLL, 22);
-
- renderAndVerify(params, "vector_drawable_91383.png", TimeUnit.SECONDS.toNanos(2));
- }
-
- /** Test activity.xml */
- @Test
- public void testScrolling() throws ClassNotFoundException {
- // Create the layout pull parser.
- LayoutPullParser parser = createLayoutPullParser("scrolled.xml");
- // Create LayoutLibCallback.
- LayoutLibTestCallback layoutLibCallback =
- new LayoutLibTestCallback(getLogger(), mDefaultClassLoader);
- layoutLibCallback.initResources();
-
- SessionParams params = getSessionParams(parser, ConfigGenerator.NEXUS_5,
- layoutLibCallback, "Theme.Material.NoActionBar.Fullscreen", false,
- RenderingMode.V_SCROLL, 22);
- params.setForceNoDecor();
- params.setExtendedViewInfoMode(true);
-
- RenderResult result = renderAndVerify(params, "scrolled.png");
- assertNotNull(result);
- assertNotNull(result.getResult());
- assertTrue(result.getResult().isSuccess());
-
- ViewInfo rootLayout = result.getRootViews().get(0);
- // Check the first box in the main LinearLayout
- assertEquals(-90, rootLayout.getChildren().get(0).getTop());
- assertEquals(-30, rootLayout.getChildren().get(0).getLeft());
- assertEquals(90, rootLayout.getChildren().get(0).getBottom());
- assertEquals(150, rootLayout.getChildren().get(0).getRight());
-
- // Check the first box within the nested LinearLayout
- assertEquals(-450, rootLayout.getChildren().get(5).getChildren().get(0).getTop());
- assertEquals(90, rootLayout.getChildren().get(5).getChildren().get(0).getLeft());
- assertEquals(-270, rootLayout.getChildren().get(5).getChildren().get(0).getBottom());
- assertEquals(690, rootLayout.getChildren().get(5).getChildren().get(0).getRight());
- }
-
- @Test
- public void testGetResourceNameVariants() throws Exception {
- // Setup
- // Create the layout pull parser for our resources (empty.xml can not be part of the test
- // app as it won't compile).
- LayoutPullParser parser = new LayoutPullParser("/empty.xml");
- // Create LayoutLibCallback.
- LayoutLibTestCallback layoutLibCallback =
- new LayoutLibTestCallback(getLogger(), mDefaultClassLoader);
- layoutLibCallback.initResources();
- SessionParams params = getSessionParams(parser, ConfigGenerator.NEXUS_4,
- layoutLibCallback, "AppTheme", true, RenderingMode.NORMAL, 22);
- AssetManager assetManager = AssetManager.getSystem();
- DisplayMetrics metrics = new DisplayMetrics();
- Configuration configuration = RenderAction.getConfiguration(params);
- Resources resources = new Resources(assetManager, metrics, configuration);
- resources.mLayoutlibCallback = params.getLayoutlibCallback();
- resources.mContext =
- new BridgeContext(params.getProjectKey(), metrics, params.getResources(),
- params.getAssets(), params.getLayoutlibCallback(), configuration,
- params.getTargetSdkVersion(), params.isRtlSupported());
- // Test
- assertEquals("android:style/ButtonBar",
- resources.getResourceName(android.R.style.ButtonBar));
- assertEquals("android", resources.getResourcePackageName(android.R.style.ButtonBar));
- assertEquals("ButtonBar", resources.getResourceEntryName(android.R.style.ButtonBar));
- assertEquals("style", resources.getResourceTypeName(android.R.style.ButtonBar));
- int id = resources.mLayoutlibCallback.getResourceId(ResourceType.STRING, "app_name");
- assertEquals("com.android.layoutlib.test.myapplication:string/app_name",
- resources.getResourceName(id));
- assertEquals("com.android.layoutlib.test.myapplication",
- resources.getResourcePackageName(id));
- assertEquals("string", resources.getResourceTypeName(id));
- assertEquals("app_name", resources.getResourceEntryName(id));
- }
-
- @NonNull
- private LayoutPullParser createLayoutPullParser(String layoutPath) {
- return new LayoutPullParser(APP_TEST_RES + "/layout/" + layoutPath);
- }
-
- @NonNull
- private static RenderResult render(SessionParams params, long frameTimeNanos) {
- // TODO: Set up action bar handler properly to test menu rendering.
- // Create session params.
- System_Delegate.setBootTimeNanos(TimeUnit.MILLISECONDS.toNanos(871732800000L));
- System_Delegate.setNanosTime(TimeUnit.MILLISECONDS.toNanos(871732800000L));
- RenderSession session = sBridge.createSession(params);
-
- try {
-
- if (frameTimeNanos != -1) {
- session.setElapsedFrameTimeNanos(frameTimeNanos);
- }
-
- if (!session.getResult().isSuccess()) {
- getLogger().error(session.getResult().getException(),
- session.getResult().getErrorMessage());
- }
- // Render the session with a timeout of 50s.
- Result renderResult = session.render(50000);
- if (!renderResult.isSuccess()) {
- getLogger().error(session.getResult().getException(),
- session.getResult().getErrorMessage());
- }
-
- return RenderResult.getFromSession(session);
- } finally {
- session.dispose();
- }
- }
-
- /**
- * Create a new rendering session and test that rendering the given layout doesn't throw any
- * exceptions and matches the provided image.
- * <p>
- * If frameTimeNanos is >= 0 a frame will be executed during the rendering. The time indicates
- * how far in the future is.
- */
- @Nullable
- private static RenderResult renderAndVerify(SessionParams params, String goldenFileName, long
- frameTimeNanos)
- throws ClassNotFoundException {
- RenderResult result = Main.render(params, frameTimeNanos);
- try {
- String goldenImagePath = APP_TEST_DIR + "/golden/" + goldenFileName;
- assertNotNull(result.getImage());
- ImageUtils.requireSimilar(goldenImagePath, result.getImage());
- } catch (IOException e) {
- getLogger().error(e, e.getMessage());
- }
-
- return result;
- }
-
- /**
- * Create a new rendering session and test that rendering the given layout doesn't throw any
- * exceptions and matches the provided image.
- */
- @Nullable
- private static RenderResult renderAndVerify(SessionParams params, String goldenFileName)
- throws ClassNotFoundException {
- return Main.renderAndVerify(params, goldenFileName, -1);
- }
-
- /**
- * Create a new rendering session and test that rendering the given layout on nexus 5
- * doesn't throw any exceptions and matches the provided image.
- */
- @Nullable
- private RenderResult renderAndVerify(String layoutFileName, String goldenFileName)
- throws ClassNotFoundException {
- return renderAndVerify(layoutFileName, goldenFileName, ConfigGenerator.NEXUS_5);
- }
-
- /**
- * Create a new rendering session and test that rendering the given layout on given device
- * doesn't throw any exceptions and matches the provided image.
- */
- @Nullable
- private RenderResult renderAndVerify(String layoutFileName, String goldenFileName,
- ConfigGenerator deviceConfig)
- throws ClassNotFoundException {
- SessionParams params = createSessionParams(layoutFileName, deviceConfig);
- return renderAndVerify(params, goldenFileName);
- }
-
- private SessionParams createSessionParams(String layoutFileName, ConfigGenerator deviceConfig)
- throws ClassNotFoundException {
- // Create the layout pull parser.
- LayoutPullParser parser = createLayoutPullParser(layoutFileName);
- // Create LayoutLibCallback.
- LayoutLibTestCallback layoutLibCallback =
- new LayoutLibTestCallback(getLogger(), mDefaultClassLoader);
- layoutLibCallback.initResources();
- // TODO: Set up action bar handler properly to test menu rendering.
- // Create session params.
- return getSessionParams(parser, deviceConfig,
- layoutLibCallback, "AppTheme", true, RenderingMode.NORMAL, 22);
- }
-
- /**
- * Uses Theme.Material and Target sdk version as 22.
- */
- private SessionParams getSessionParams(LayoutPullParser layoutParser,
- ConfigGenerator configGenerator, LayoutLibTestCallback layoutLibCallback,
- String themeName, boolean isProjectTheme, RenderingMode renderingMode, int targetSdk) {
- FolderConfiguration config = configGenerator.getFolderConfig();
- ResourceResolver resourceResolver =
- ResourceResolver.create(sProjectResources.getConfiguredResources(config),
- sFrameworkRepo.getConfiguredResources(config),
- themeName, isProjectTheme);
-
- SessionParams sessionParams = new SessionParams(
- layoutParser,
- renderingMode,
- null /*used for caching*/,
- configGenerator.getHardwareConfig(),
- resourceResolver,
- layoutLibCallback,
- 0,
- targetSdk,
- getLayoutLog());
- sessionParams.setFlag(RenderParamsFlags.FLAG_DO_NOT_RENDER_ON_CREATE, true);
- return sessionParams;
- }
-
- private static LayoutLog getLayoutLog() {
- if (sLayoutLibLog == null) {
- sLayoutLibLog = new LayoutLog() {
- @Override
- public void warning(String tag, String message, Object data) {
- System.out.println("Warning " + tag + ": " + message);
- failWithMsg(message);
- }
-
- @Override
- public void fidelityWarning(@Nullable String tag, String message,
- Throwable throwable, Object data) {
-
- System.out.println("FidelityWarning " + tag + ": " + message);
- if (throwable != null) {
- throwable.printStackTrace();
- }
- failWithMsg(message == null ? "" : message);
- }
-
- @Override
- public void error(String tag, String message, Object data) {
- System.out.println("Error " + tag + ": " + message);
- failWithMsg(message);
- }
-
- @Override
- public void error(String tag, String message, Throwable throwable, Object data) {
- System.out.println("Error " + tag + ": " + message);
- if (throwable != null) {
- throwable.printStackTrace();
- }
- failWithMsg(message);
- }
- };
- }
- return sLayoutLibLog;
- }
-
- private static ILogger getLogger() {
- if (sLogger == null) {
- sLogger = new ILogger() {
- @Override
- public void error(Throwable t, @Nullable String msgFormat, Object... args) {
- if (t != null) {
- t.printStackTrace();
- }
- failWithMsg(msgFormat == null ? "" : msgFormat, args);
- }
-
- @Override
- public void warning(@NonNull String msgFormat, Object... args) {
- failWithMsg(msgFormat, args);
- }
-
- @Override
- public void info(@NonNull String msgFormat, Object... args) {
- // pass.
- }
-
- @Override
- public void verbose(@NonNull String msgFormat, Object... args) {
- // pass.
- }
- };
- }
- return sLogger;
- }
-
- private static void failWithMsg(@NonNull String msgFormat, Object... args) {
- sRenderMessages.add(args == null ? msgFormat : String.format(msgFormat, args));
- }
}
diff --git a/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/PerformanceTests.java b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/PerformanceTests.java
new file mode 100644
index 0000000..c90c26a
--- /dev/null
+++ b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/PerformanceTests.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2016 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.layoutlib.bridge.intensive;
+
+import com.android.ide.common.rendering.api.SessionParams;
+import com.android.layoutlib.bridge.intensive.setup.ConfigGenerator;
+import com.android.layoutlib.bridge.intensive.util.perf.PerformanceRunner;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import android.annotation.NonNull;
+
+/**
+ * Set of render tests
+ */
+@RunWith(PerformanceRunner.class)
+public class PerformanceTests extends RenderTestBase {
+
+ @Before
+ public void setUp() {
+ ignoreAllLogging();
+ }
+
+
+ private void render(@NonNull String layoutFileName) throws ClassNotFoundException {
+ SessionParams params = createSessionParams(layoutFileName, ConfigGenerator.NEXUS_5);
+ render(params, 250);
+ }
+
+ @Test
+ public void testActivity() throws ClassNotFoundException {
+ render("activity.xml");
+ }
+
+ @Test
+ public void testAllWidgets() throws ClassNotFoundException {
+ render("allwidgets.xml");
+ }
+}
diff --git a/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/RenderTestBase.java b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/RenderTestBase.java
new file mode 100644
index 0000000..3e5f9e0
--- /dev/null
+++ b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/RenderTestBase.java
@@ -0,0 +1,542 @@
+/*
+ * Copyright (C) 2016 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.layoutlib.bridge.intensive;
+
+import com.android.ide.common.rendering.api.LayoutLog;
+import com.android.ide.common.rendering.api.RenderSession;
+import com.android.ide.common.rendering.api.Result;
+import com.android.ide.common.rendering.api.SessionParams;
+import com.android.ide.common.rendering.api.SessionParams.RenderingMode;
+import com.android.ide.common.resources.FrameworkResources;
+import com.android.ide.common.resources.ResourceItem;
+import com.android.ide.common.resources.ResourceRepository;
+import com.android.ide.common.resources.ResourceResolver;
+import com.android.ide.common.resources.configuration.FolderConfiguration;
+import com.android.io.FolderWrapper;
+import com.android.layoutlib.bridge.Bridge;
+import com.android.layoutlib.bridge.android.RenderParamsFlags;
+import com.android.layoutlib.bridge.impl.DelegateManager;
+import com.android.layoutlib.bridge.intensive.setup.ConfigGenerator;
+import com.android.layoutlib.bridge.intensive.setup.LayoutLibTestCallback;
+import com.android.layoutlib.bridge.intensive.setup.LayoutPullParser;
+import com.android.layoutlib.bridge.intensive.util.ImageUtils;
+import com.android.layoutlib.bridge.intensive.util.ModuleClassLoader;
+import com.android.layoutlib.bridge.intensive.util.TestUtils;
+import com.android.tools.layoutlib.java.System_Delegate;
+import com.android.utils.ILogger;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.rules.TestWatcher;
+import org.junit.runner.Description;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.concurrent.TimeUnit;
+
+import com.google.android.collect.Lists;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+
+/**
+ * Base class for render tests. The render tests load all the framework resources and a project
+ * checked in this test's resources. The main dependencies
+ * are:
+ * 1. Fonts directory.
+ * 2. Framework Resources.
+ * 3. App resources.
+ * 4. build.prop file
+ * <p>
+ * These are configured by two variables set in the system properties.
+ * <p>
+ * 1. platform.dir: This is the directory for the current platform in the built SDK
+ * (.../sdk/platforms/android-<version>).
+ * <p>
+ * The fonts are platform.dir/data/fonts.
+ * The Framework resources are platform.dir/data/res.
+ * build.prop is at platform.dir/build.prop.
+ * <p>
+ * 2. test_res.dir: This is the directory for the resources of the test. If not specified, this
+ * falls back to getClass().getProtectionDomain().getCodeSource().getLocation()
+ * <p>
+ * The app resources are at: test_res.dir/testApp/MyApplication/app/src/main/res
+ */
+public class RenderTestBase {
+
+ private static final String PLATFORM_DIR_PROPERTY = "platform.dir";
+ private static final String RESOURCE_DIR_PROPERTY = "test_res.dir";
+
+ private static final String PLATFORM_DIR;
+ private static final String TEST_RES_DIR;
+ /** Location of the app to test inside {@link #TEST_RES_DIR} */
+ private static final String APP_TEST_DIR = "testApp/MyApplication";
+ /** Location of the app's res dir inside {@link #TEST_RES_DIR} */
+ private static final String APP_TEST_RES = APP_TEST_DIR + "/src/main/res";
+ private static final String APP_CLASSES_LOCATION =
+ APP_TEST_DIR + "/build/intermediates/classes/debug/";
+ protected static Bridge sBridge;
+ /** List of log messages generated by a render call. It can be used to find specific errors */
+ protected static ArrayList<String> sRenderMessages = Lists.newArrayList();
+ private static LayoutLog sLayoutLibLog;
+ private static FrameworkResources sFrameworkRepo;
+ private static ResourceRepository sProjectResources;
+ private static ILogger sLogger;
+
+ static {
+ // Test that System Properties are properly set.
+ PLATFORM_DIR = getPlatformDir();
+ if (PLATFORM_DIR == null) {
+ fail(String.format("System Property %1$s not properly set. The value is %2$s",
+ PLATFORM_DIR_PROPERTY, System.getProperty(PLATFORM_DIR_PROPERTY)));
+ }
+
+ TEST_RES_DIR = getTestResDir();
+ if (TEST_RES_DIR == null) {
+ fail(String.format("System property %1$s.dir not properly set. The value is %2$s",
+ RESOURCE_DIR_PROPERTY, System.getProperty(RESOURCE_DIR_PROPERTY)));
+ }
+ }
+
+ @Rule
+ public TestWatcher sRenderMessageWatcher = new TestWatcher() {
+ @Override
+ protected void succeeded(Description description) {
+ // We only check error messages if the rest of the test case was successful.
+ if (!sRenderMessages.isEmpty()) {
+ fail(description.getMethodName() + " render error message: " +
+ sRenderMessages.get(0));
+ }
+ }
+ };
+ // Default class loader with access to the app classes
+ protected ClassLoader mDefaultClassLoader =
+ new ModuleClassLoader(APP_CLASSES_LOCATION, getClass().getClassLoader());
+
+ private static String getPlatformDir() {
+ String platformDir = System.getProperty(PLATFORM_DIR_PROPERTY);
+ if (platformDir != null && !platformDir.isEmpty() && new File(platformDir).isDirectory()) {
+ return platformDir;
+ }
+ // System Property not set. Try to find the directory in the build directory.
+ String androidHostOut = System.getenv("ANDROID_HOST_OUT");
+ if (androidHostOut != null) {
+ platformDir = getPlatformDirFromHostOut(new File(androidHostOut));
+ if (platformDir != null) {
+ return platformDir;
+ }
+ }
+ String workingDirString = System.getProperty("user.dir");
+ File workingDir = new File(workingDirString);
+ // Test if workingDir is android checkout root.
+ platformDir = getPlatformDirFromRoot(workingDir);
+ if (platformDir != null) {
+ return platformDir;
+ }
+
+ // Test if workingDir is platform/frameworks/base/tools/layoutlib/bridge.
+ File currentDir = workingDir;
+ if (currentDir.getName().equalsIgnoreCase("bridge")) {
+ currentDir = currentDir.getParentFile();
+ }
+ // Test if currentDir is platform/frameworks/base/tools/layoutlib. That is, root should be
+ // workingDir/../../../../ (4 levels up)
+ for (int i = 0; i < 4; i++) {
+ if (currentDir != null) {
+ currentDir = currentDir.getParentFile();
+ }
+ }
+ return currentDir == null ? null : getPlatformDirFromRoot(currentDir);
+ }
+
+ private static String getPlatformDirFromRoot(File root) {
+ if (!root.isDirectory()) {
+ return null;
+ }
+ File out = new File(root, "out");
+ if (!out.isDirectory()) {
+ return null;
+ }
+ File host = new File(out, "host");
+ if (!host.isDirectory()) {
+ return null;
+ }
+ File[] hosts = host.listFiles(path -> path.isDirectory() &&
+ (path.getName().startsWith("linux-") || path.getName().startsWith("darwin-")));
+ assert hosts != null;
+ for (File hostOut : hosts) {
+ String platformDir = getPlatformDirFromHostOut(hostOut);
+ if (platformDir != null) {
+ return platformDir;
+ }
+ }
+ return null;
+ }
+
+ private static String getPlatformDirFromHostOut(File out) {
+ if (!out.isDirectory()) {
+ return null;
+ }
+ File sdkDir = new File(out, "sdk");
+ if (!sdkDir.isDirectory()) {
+ return null;
+ }
+ File[] sdkDirs = sdkDir.listFiles(path -> {
+ // We need to search for $TARGET_PRODUCT (usually, sdk_phone_armv7)
+ return path.isDirectory() && path.getName().startsWith("sdk");
+ });
+ assert sdkDirs != null;
+ for (File dir : sdkDirs) {
+ String platformDir = getPlatformDirFromHostOutSdkSdk(dir);
+ if (platformDir != null) {
+ return platformDir;
+ }
+ }
+ return null;
+ }
+
+ private static String getPlatformDirFromHostOutSdkSdk(File sdkDir) {
+ File[] possibleSdks = sdkDir.listFiles(
+ path -> path.isDirectory() && path.getName().contains("android-sdk"));
+ assert possibleSdks != null;
+ for (File possibleSdk : possibleSdks) {
+ File platformsDir = new File(possibleSdk, "platforms");
+ File[] platforms = platformsDir.listFiles(
+ path -> path.isDirectory() && path.getName().startsWith("android-"));
+ if (platforms == null || platforms.length == 0) {
+ continue;
+ }
+ Arrays.sort(platforms, (o1, o2) -> {
+ final int MAX_VALUE = 1000;
+ String suffix1 = o1.getName().substring("android-".length());
+ String suffix2 = o2.getName().substring("android-".length());
+ int suff1, suff2;
+ try {
+ suff1 = Integer.parseInt(suffix1);
+ } catch (NumberFormatException e) {
+ suff1 = MAX_VALUE;
+ }
+ try {
+ suff2 = Integer.parseInt(suffix2);
+ } catch (NumberFormatException e) {
+ suff2 = MAX_VALUE;
+ }
+ if (suff1 != MAX_VALUE || suff2 != MAX_VALUE) {
+ return suff2 - suff1;
+ }
+ return suffix2.compareTo(suffix1);
+ });
+ return platforms[0].getAbsolutePath();
+ }
+ return null;
+ }
+
+ private static String getTestResDir() {
+ String resourceDir = System.getProperty(RESOURCE_DIR_PROPERTY);
+ if (resourceDir != null && !resourceDir.isEmpty() && new File(resourceDir).isDirectory()) {
+ return resourceDir;
+ }
+ // TEST_RES_DIR not explicitly set. Fallback to the class's source location.
+ try {
+ URL location = RenderTestBase.class.getProtectionDomain().getCodeSource().getLocation();
+ return new File(location.getPath()).exists() ? location.getPath() : null;
+ } catch (NullPointerException e) {
+ // Prevent a lot of null checks by just catching the exception.
+ return null;
+ }
+ }
+
+ /**
+ * Initialize the bridge and the resource maps.
+ */
+ @BeforeClass
+ public static void beforeClass() {
+ File data_dir = new File(PLATFORM_DIR, "data");
+ File res = new File(data_dir, "res");
+ sFrameworkRepo = new FrameworkResources(new FolderWrapper(res));
+ sFrameworkRepo.loadResources();
+ sFrameworkRepo.loadPublicResources(getLogger());
+
+ sProjectResources =
+ new ResourceRepository(new FolderWrapper(TEST_RES_DIR + "/" + APP_TEST_RES),
+ false) {
+ @NonNull
+ @Override
+ protected ResourceItem createResourceItem(@NonNull String name) {
+ return new ResourceItem(name);
+ }
+ };
+ sProjectResources.loadResources();
+
+ File fontLocation = new File(data_dir, "fonts");
+ File buildProp = new File(PLATFORM_DIR, "build.prop");
+ File attrs = new File(res, "values" + File.separator + "attrs.xml");
+ sBridge = new Bridge();
+ sBridge.init(ConfigGenerator.loadProperties(buildProp), fontLocation,
+ ConfigGenerator.getEnumMap(attrs), getLayoutLog());
+ Bridge.getLock().lock();
+ try {
+ Bridge.setLog(getLayoutLog());
+ } finally {
+ Bridge.getLock().unlock();
+ }
+ }
+
+ @AfterClass
+ public static void tearDown() {
+ sLayoutLibLog = null;
+ sFrameworkRepo = null;
+ sProjectResources = null;
+ sLogger = null;
+ sBridge = null;
+
+ TestUtils.gc();
+
+ System.out.println("Objects still linked from the DelegateManager:");
+ DelegateManager.dump(System.out);
+ }
+
+ @NonNull
+ protected static RenderResult render(SessionParams params, long frameTimeNanos) {
+ // TODO: Set up action bar handler properly to test menu rendering.
+ // Create session params.
+ System_Delegate.setBootTimeNanos(TimeUnit.MILLISECONDS.toNanos(871732800000L));
+ System_Delegate.setNanosTime(TimeUnit.MILLISECONDS.toNanos(871732800000L));
+ RenderSession session = sBridge.createSession(params);
+
+ try {
+
+ if (frameTimeNanos != -1) {
+ session.setElapsedFrameTimeNanos(frameTimeNanos);
+ }
+
+ if (!session.getResult().isSuccess()) {
+ getLogger().error(session.getResult().getException(),
+ session.getResult().getErrorMessage());
+ }
+ // Render the session with a timeout of 50s.
+ Result renderResult = session.render(50000);
+ if (!renderResult.isSuccess()) {
+ getLogger().error(session.getResult().getException(),
+ session.getResult().getErrorMessage());
+ }
+
+ return RenderResult.getFromSession(session);
+ } finally {
+ session.dispose();
+ }
+ }
+
+ /**
+ * Create a new rendering session and test that rendering the given layout doesn't throw any
+ * exceptions and matches the provided image.
+ * <p>
+ * If frameTimeNanos is >= 0 a frame will be executed during the rendering. The time indicates
+ * how far in the future is.
+ */
+ @Nullable
+ protected static RenderResult renderAndVerify(SessionParams params, String goldenFileName,
+ long frameTimeNanos) throws ClassNotFoundException {
+ RenderResult result = RenderTestBase.render(params, frameTimeNanos);
+ try {
+ String goldenImagePath = APP_TEST_DIR + "/golden/" + goldenFileName;
+ assertNotNull(result.getImage());
+ ImageUtils.requireSimilar(goldenImagePath, result.getImage());
+ } catch (IOException e) {
+ getLogger().error(e, e.getMessage());
+ }
+
+ return result;
+ }
+
+ /**
+ * Create a new rendering session and test that rendering the given layout doesn't throw any
+ * exceptions and matches the provided image.
+ */
+ @Nullable
+ protected static RenderResult renderAndVerify(SessionParams params, String goldenFileName)
+ throws ClassNotFoundException {
+ return RenderTestBase.renderAndVerify(params, goldenFileName, -1);
+ }
+
+ private static LayoutLog getLayoutLog() {
+ if (sLayoutLibLog == null) {
+ sLayoutLibLog = new LayoutLog() {
+ @Override
+ public void warning(String tag, String message, Object data) {
+ System.out.println("Warning " + tag + ": " + message);
+ failWithMsg(message);
+ }
+
+ @Override
+ public void fidelityWarning(@Nullable String tag, String message,
+ Throwable throwable, Object data) {
+
+ System.out.println("FidelityWarning " + tag + ": " + message);
+ if (throwable != null) {
+ throwable.printStackTrace();
+ }
+ failWithMsg(message == null ? "" : message);
+ }
+
+ @Override
+ public void error(String tag, String message, Object data) {
+ System.out.println("Error " + tag + ": " + message);
+ failWithMsg(message);
+ }
+
+ @Override
+ public void error(String tag, String message, Throwable throwable, Object data) {
+ System.out.println("Error " + tag + ": " + message);
+ if (throwable != null) {
+ throwable.printStackTrace();
+ }
+ failWithMsg(message);
+ }
+ };
+ }
+ return sLayoutLibLog;
+ }
+
+ protected static void ignoreAllLogging() {
+ sLayoutLibLog = new LayoutLog();
+ sLogger = new ILogger() {
+ @Override
+ public void error(Throwable t, String msgFormat, Object... args) {
+ }
+
+ @Override
+ public void warning(String msgFormat, Object... args) {
+ }
+
+ @Override
+ public void info(String msgFormat, Object... args) {
+ }
+
+ @Override
+ public void verbose(String msgFormat, Object... args) {
+ }
+ };
+ }
+
+ protected static ILogger getLogger() {
+ if (sLogger == null) {
+ sLogger = new ILogger() {
+ @Override
+ public void error(Throwable t, @Nullable String msgFormat, Object... args) {
+ if (t != null) {
+ t.printStackTrace();
+ }
+ failWithMsg(msgFormat == null ? "" : msgFormat, args);
+ }
+
+ @Override
+ public void warning(@NonNull String msgFormat, Object... args) {
+ failWithMsg(msgFormat, args);
+ }
+
+ @Override
+ public void info(@NonNull String msgFormat, Object... args) {
+ // pass.
+ }
+
+ @Override
+ public void verbose(@NonNull String msgFormat, Object... args) {
+ // pass.
+ }
+ };
+ }
+ return sLogger;
+ }
+
+ private static void failWithMsg(@NonNull String msgFormat, Object... args) {
+ sRenderMessages.add(args == null ? msgFormat : String.format(msgFormat, args));
+ }
+
+ @Before
+ public void beforeTestCase() {
+ sRenderMessages.clear();
+ }
+
+ @NonNull
+ protected LayoutPullParser createLayoutPullParser(String layoutPath) {
+ return new LayoutPullParser(APP_TEST_RES + "/layout/" + layoutPath);
+ }
+
+ /**
+ * Create a new rendering session and test that rendering the given layout on nexus 5
+ * doesn't throw any exceptions and matches the provided image.
+ */
+ @Nullable
+ protected RenderResult renderAndVerify(String layoutFileName, String goldenFileName)
+ throws ClassNotFoundException {
+ return renderAndVerify(layoutFileName, goldenFileName, ConfigGenerator.NEXUS_5);
+ }
+
+ /**
+ * Create a new rendering session and test that rendering the given layout on given device
+ * doesn't throw any exceptions and matches the provided image.
+ */
+ @Nullable
+ protected RenderResult renderAndVerify(String layoutFileName, String goldenFileName,
+ ConfigGenerator deviceConfig) throws ClassNotFoundException {
+ SessionParams params = createSessionParams(layoutFileName, deviceConfig);
+ return renderAndVerify(params, goldenFileName);
+ }
+
+ protected SessionParams createSessionParams(String layoutFileName, ConfigGenerator deviceConfig)
+ throws ClassNotFoundException {
+ // Create the layout pull parser.
+ LayoutPullParser parser = createLayoutPullParser(layoutFileName);
+ // Create LayoutLibCallback.
+ LayoutLibTestCallback layoutLibCallback =
+ new LayoutLibTestCallback(getLogger(), mDefaultClassLoader);
+ layoutLibCallback.initResources();
+ // TODO: Set up action bar handler properly to test menu rendering.
+ // Create session params.
+ return getSessionParams(parser, deviceConfig, layoutLibCallback, "AppTheme", true,
+ RenderingMode.NORMAL, 22);
+ }
+
+ /**
+ * Uses Theme.Material and Target sdk version as 22.
+ */
+ protected SessionParams getSessionParams(LayoutPullParser layoutParser,
+ ConfigGenerator configGenerator, LayoutLibTestCallback layoutLibCallback,
+ String themeName, boolean isProjectTheme, RenderingMode renderingMode,
+ @SuppressWarnings("SameParameterValue") int targetSdk) {
+ FolderConfiguration config = configGenerator.getFolderConfig();
+ ResourceResolver resourceResolver =
+ ResourceResolver.create(sProjectResources.getConfiguredResources(config),
+ sFrameworkRepo.getConfiguredResources(config), themeName, isProjectTheme);
+
+ SessionParams sessionParams =
+ new SessionParams(layoutParser, renderingMode, null /*used for caching*/,
+ configGenerator.getHardwareConfig(), resourceResolver, layoutLibCallback, 0,
+ targetSdk, getLayoutLog());
+ sessionParams.setFlag(RenderParamsFlags.FLAG_DO_NOT_RENDER_ON_CREATE, true);
+ return sessionParams;
+ }
+}
diff --git a/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/RenderTests.java b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/RenderTests.java
new file mode 100644
index 0000000..73e51ec
--- /dev/null
+++ b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/RenderTests.java
@@ -0,0 +1,387 @@
+/*
+ * Copyright (C) 2014 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.layoutlib.bridge.intensive;
+
+import com.android.ide.common.rendering.api.RenderSession;
+import com.android.ide.common.rendering.api.SessionParams;
+import com.android.ide.common.rendering.api.SessionParams.RenderingMode;
+import com.android.ide.common.rendering.api.ViewInfo;
+import com.android.layoutlib.bridge.android.BridgeContext;
+import com.android.layoutlib.bridge.android.RenderParamsFlags;
+import com.android.layoutlib.bridge.impl.RenderAction;
+import com.android.layoutlib.bridge.intensive.setup.ConfigGenerator;
+import com.android.layoutlib.bridge.intensive.setup.LayoutLibTestCallback;
+import com.android.layoutlib.bridge.intensive.setup.LayoutPullParser;
+import com.android.resources.Density;
+import com.android.resources.Navigation;
+import com.android.resources.ResourceType;
+
+import org.junit.Test;
+
+import android.content.res.AssetManager;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.util.DisplayMetrics;
+
+import java.lang.reflect.Field;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Set of render tests
+ */
+public class RenderTests extends RenderTestBase {
+
+ @Test
+ public void testActivity() throws ClassNotFoundException {
+ renderAndVerify("activity.xml", "activity.png");
+ }
+
+ @Test
+ public void testActivityOnOldTheme() throws ClassNotFoundException {
+ LayoutLibTestCallback layoutLibCallback =
+ new LayoutLibTestCallback(getLogger(), mDefaultClassLoader);
+ layoutLibCallback.initResources();
+
+ LayoutPullParser parser = createLayoutPullParser("simple_activity.xml");
+ SessionParams params = getSessionParams(parser, ConfigGenerator.NEXUS_5,
+ layoutLibCallback, "Theme.NoTitleBar", false,
+ RenderingMode.NORMAL, 22);
+
+ renderAndVerify(params, "simple_activity-old-theme.png");
+ }
+
+ @Test
+ public void testTranslucentBars() throws ClassNotFoundException {
+ LayoutLibTestCallback layoutLibCallback =
+ new LayoutLibTestCallback(getLogger(), mDefaultClassLoader);
+ layoutLibCallback.initResources();
+
+ LayoutPullParser parser = createLayoutPullParser("four_corners.xml");
+ SessionParams params = getSessionParams(parser, ConfigGenerator.NEXUS_5,
+ layoutLibCallback, "Theme.Material.Light.NoActionBar.TranslucentDecor", false,
+ RenderingMode.NORMAL, 22);
+ renderAndVerify(params, "four_corners_translucent.png");
+
+ parser = createLayoutPullParser("four_corners.xml");
+ params = getSessionParams(parser, ConfigGenerator.NEXUS_5_LAND,
+ layoutLibCallback, "Theme.Material.Light.NoActionBar.TranslucentDecor", false,
+ RenderingMode.NORMAL, 22);
+ renderAndVerify(params, "four_corners_translucent_land.png");
+
+ parser = createLayoutPullParser("four_corners.xml");
+ params = getSessionParams(parser, ConfigGenerator.NEXUS_5,
+ layoutLibCallback, "Theme.Material.Light.NoActionBar", false,
+ RenderingMode.NORMAL, 22);
+ renderAndVerify(params, "four_corners.png");
+ }
+
+ @Test
+ public void testAllWidgets() throws ClassNotFoundException {
+ renderAndVerify("allwidgets.xml", "allwidgets.png");
+
+ // We expect fidelity warnings for Path.isConvex. Fail for anything else.
+ sRenderMessages.removeIf(message -> message.equals("Path.isConvex is not supported."));
+ }
+
+ @Test
+ public void testArrayCheck() throws ClassNotFoundException {
+ renderAndVerify("array_check.xml", "array_check.png");
+ }
+
+ @Test
+ public void testAllWidgetsTablet() throws ClassNotFoundException {
+ renderAndVerify("allwidgets.xml", "allwidgets_tab.png", ConfigGenerator.NEXUS_7_2012);
+
+ // We expect fidelity warnings for Path.isConvex. Fail for anything else.
+ sRenderMessages.removeIf(message -> message.equals("Path.isConvex is not supported."));
+ }
+
+ @Test
+ public void testActivityActionBar() throws ClassNotFoundException {
+ LayoutPullParser parser = createLayoutPullParser("simple_activity.xml");
+ LayoutLibTestCallback layoutLibCallback =
+ new LayoutLibTestCallback(getLogger(), mDefaultClassLoader);
+ layoutLibCallback.initResources();
+
+ SessionParams params = getSessionParams(parser, ConfigGenerator.NEXUS_5,
+ layoutLibCallback, "Theme.Material.Light.NoActionBar", false,
+ RenderingMode.V_SCROLL, 22);
+
+ renderAndVerify(params, "simple_activity_noactionbar.png");
+
+ parser = createLayoutPullParser("simple_activity.xml");
+ params = getSessionParams(parser, ConfigGenerator.NEXUS_5,
+ layoutLibCallback, "Theme.Material.Light", false,
+ RenderingMode.V_SCROLL, 22);
+
+ renderAndVerify(params, "simple_activity.png");
+
+ // This also tests that a theme with "NoActionBar" DOES HAVE an action bar when we are
+ // displaying menus.
+ parser = createLayoutPullParser("simple_activity.xml");
+ params = getSessionParams(parser, ConfigGenerator.NEXUS_5,
+ layoutLibCallback, "Theme.Material.Light.NoActionBar", false,
+ RenderingMode.V_SCROLL, 22);
+ params.setFlag(RenderParamsFlags.FLAG_KEY_ROOT_TAG, "menu");
+ renderAndVerify(params, "simple_activity.png");
+ }
+
+ @Test
+ public void testOnApplyInsetsCall()
+ throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
+ // We get the widget via reflection to avoid IntelliJ complaining about the class being
+ // located in the wrong package. (From the Bridge tests point of view, it is)
+ Class insetsWidgetClass = Class.forName("com.android.layoutlib.test.myapplication.widgets" +
+ ".InsetsWidget");
+ Field field = insetsWidgetClass.getDeclaredField("sApplyInsetsCalled");
+ assertFalse((Boolean)field.get(null));
+
+ LayoutPullParser parser = createLayoutPullParser("insets.xml");
+ LayoutLibTestCallback layoutLibCallback =
+ new LayoutLibTestCallback(getLogger(), mDefaultClassLoader);
+ layoutLibCallback.initResources();
+ SessionParams params = getSessionParams(parser, ConfigGenerator.NEXUS_5,
+ layoutLibCallback, "Theme.Material.Light.NoActionBar", false,
+ RenderingMode.NORMAL, 22);
+
+ render(params, -1);
+
+ assertTrue((Boolean)field.get(null));
+ field.set(null, false);
+ }
+
+ /** Test expand_layout.xml */
+ @Test
+ public void testExpand() throws ClassNotFoundException {
+ // Create the layout pull parser.
+ LayoutPullParser parser = createLayoutPullParser("expand_vert_layout.xml");
+ // Create LayoutLibCallback.
+ LayoutLibTestCallback layoutLibCallback =
+ new LayoutLibTestCallback(getLogger(), mDefaultClassLoader);
+ layoutLibCallback.initResources();
+
+ ConfigGenerator customConfigGenerator = new ConfigGenerator()
+ .setScreenWidth(300)
+ .setScreenHeight(20)
+ .setDensity(Density.XHIGH)
+ .setNavigation(Navigation.NONAV);
+
+ SessionParams params = getSessionParams(parser, customConfigGenerator,
+ layoutLibCallback, "Theme.Material.Light.NoActionBar.Fullscreen", false,
+ RenderingMode.V_SCROLL, 22);
+
+ renderAndVerify(params, "expand_vert_layout.png");
+
+ customConfigGenerator = new ConfigGenerator()
+ .setScreenWidth(20)
+ .setScreenHeight(300)
+ .setDensity(Density.XHIGH)
+ .setNavigation(Navigation.NONAV);
+ parser = createLayoutPullParser("expand_horz_layout.xml");
+ params = getSessionParams(parser, customConfigGenerator,
+ layoutLibCallback, "Theme.Material.Light.NoActionBar.Fullscreen", false,
+ RenderingMode.H_SCROLL, 22);
+
+ renderAndVerify(params, "expand_horz_layout.png");
+ }
+
+ /** Test indeterminate_progressbar.xml */
+ @Test
+ public void testVectorAnimation() throws ClassNotFoundException {
+ // Create the layout pull parser.
+ LayoutPullParser parser = createLayoutPullParser("indeterminate_progressbar.xml");
+ // Create LayoutLibCallback.
+ LayoutLibTestCallback layoutLibCallback =
+ new LayoutLibTestCallback(getLogger(), mDefaultClassLoader);
+ layoutLibCallback.initResources();
+
+ SessionParams params = getSessionParams(parser, ConfigGenerator.NEXUS_5,
+ layoutLibCallback, "Theme.Material.NoActionBar.Fullscreen", false,
+ RenderingMode.V_SCROLL, 22);
+
+ renderAndVerify(params, "animated_vector.png", TimeUnit.SECONDS.toNanos(2));
+
+ parser = createLayoutPullParser("indeterminate_progressbar.xml");
+ params = getSessionParams(parser, ConfigGenerator.NEXUS_5,
+ layoutLibCallback, "Theme.Material.NoActionBar.Fullscreen", false,
+ RenderingMode.V_SCROLL, 22);
+ renderAndVerify(params, "animated_vector_1.png", TimeUnit.SECONDS.toNanos(3));
+ }
+
+ /**
+ * Test a vector drawable that uses trimStart and trimEnd. It also tests all the primitives
+ * for vector drawables (lines, moves and cubic and quadratic curves).
+ */
+ @Test
+ public void testVectorDrawable() throws ClassNotFoundException {
+ // Create the layout pull parser.
+ LayoutPullParser parser = createLayoutPullParser("vector_drawable.xml");
+ // Create LayoutLibCallback.
+ LayoutLibTestCallback layoutLibCallback =
+ new LayoutLibTestCallback(getLogger(), mDefaultClassLoader);
+ layoutLibCallback.initResources();
+
+ SessionParams params = getSessionParams(parser, ConfigGenerator.NEXUS_5,
+ layoutLibCallback, "Theme.Material.NoActionBar.Fullscreen", false,
+ RenderingMode.V_SCROLL, 22);
+
+ renderAndVerify(params, "vector_drawable.png", TimeUnit.SECONDS.toNanos(2));
+ }
+
+ /**
+ * Regression test for http://b.android.com/91383 and http://b.android.com/203797
+ */
+ @Test
+ public void testVectorDrawable91383() throws ClassNotFoundException {
+ // Create the layout pull parser.
+ LayoutPullParser parser = createLayoutPullParser("vector_drawable_android.xml");
+ // Create LayoutLibCallback.
+ LayoutLibTestCallback layoutLibCallback =
+ new LayoutLibTestCallback(getLogger(), mDefaultClassLoader);
+ layoutLibCallback.initResources();
+
+ SessionParams params = getSessionParams(parser, ConfigGenerator.NEXUS_5,
+ layoutLibCallback, "Theme.Material.NoActionBar.Fullscreen", false,
+ RenderingMode.V_SCROLL, 22);
+
+ renderAndVerify(params, "vector_drawable_91383.png", TimeUnit.SECONDS.toNanos(2));
+ }
+
+ /** Test activity.xml */
+ @Test
+ public void testScrollingAndMeasure() throws ClassNotFoundException {
+ // Create the layout pull parser.
+ LayoutPullParser parser = createLayoutPullParser("scrolled.xml");
+ // Create LayoutLibCallback.
+ LayoutLibTestCallback layoutLibCallback =
+ new LayoutLibTestCallback(getLogger(), mDefaultClassLoader);
+ layoutLibCallback.initResources();
+
+ SessionParams params = getSessionParams(parser, ConfigGenerator.NEXUS_5,
+ layoutLibCallback, "Theme.Material.NoActionBar.Fullscreen", false,
+ RenderingMode.V_SCROLL, 22);
+ params.setForceNoDecor();
+ params.setExtendedViewInfoMode(true);
+
+ // Do an only-measure pass
+ RenderSession session = sBridge.createSession(params);
+ session.measure();
+ RenderResult result = RenderResult.getFromSession(session);
+ assertNotNull(result);
+ assertNotNull(result.getResult());
+ assertTrue(result.getResult().isSuccess());
+
+ ViewInfo rootLayout = result.getRootViews().get(0);
+ // Check the first box in the main LinearLayout
+ assertEquals(-90, rootLayout.getChildren().get(0).getTop());
+ assertEquals(-30, rootLayout.getChildren().get(0).getLeft());
+ assertEquals(90, rootLayout.getChildren().get(0).getBottom());
+ assertEquals(150, rootLayout.getChildren().get(0).getRight());
+
+ // Check the first box within the nested LinearLayout
+ assertEquals(-450, rootLayout.getChildren().get(5).getChildren().get(0).getTop());
+ assertEquals(90, rootLayout.getChildren().get(5).getChildren().get(0).getLeft());
+ assertEquals(-270, rootLayout.getChildren().get(5).getChildren().get(0).getBottom());
+ assertEquals(690, rootLayout.getChildren().get(5).getChildren().get(0).getRight());
+
+ // Do a full render pass
+ parser = createLayoutPullParser("scrolled.xml");
+
+ params = getSessionParams(parser, ConfigGenerator.NEXUS_5,
+ layoutLibCallback, "Theme.Material.NoActionBar.Fullscreen", false,
+ RenderingMode.V_SCROLL, 22);
+ params.setForceNoDecor();
+ params.setExtendedViewInfoMode(true);
+
+ result = renderAndVerify(params, "scrolled.png");
+ assertNotNull(result);
+ assertNotNull(result.getResult());
+ assertTrue(result.getResult().isSuccess());
+ }
+
+ @Test
+ public void testGetResourceNameVariants() throws Exception {
+ // Setup
+ // Create the layout pull parser for our resources (empty.xml can not be part of the test
+ // app as it won't compile).
+ LayoutPullParser parser = new LayoutPullParser("/empty.xml");
+ // Create LayoutLibCallback.
+ LayoutLibTestCallback layoutLibCallback =
+ new LayoutLibTestCallback(getLogger(), mDefaultClassLoader);
+ layoutLibCallback.initResources();
+ SessionParams params = getSessionParams(parser, ConfigGenerator.NEXUS_4,
+ layoutLibCallback, "AppTheme", true, RenderingMode.NORMAL, 22);
+ AssetManager assetManager = AssetManager.getSystem();
+ DisplayMetrics metrics = new DisplayMetrics();
+ Configuration configuration = RenderAction.getConfiguration(params);
+ Resources resources = new Resources(assetManager, metrics, configuration);
+ resources.mLayoutlibCallback = params.getLayoutlibCallback();
+ resources.mContext =
+ new BridgeContext(params.getProjectKey(), metrics, params.getResources(),
+ params.getAssets(), params.getLayoutlibCallback(), configuration,
+ params.getTargetSdkVersion(), params.isRtlSupported());
+ // Test
+ assertEquals("android:style/ButtonBar",
+ resources.getResourceName(android.R.style.ButtonBar));
+ assertEquals("android", resources.getResourcePackageName(android.R.style.ButtonBar));
+ assertEquals("ButtonBar", resources.getResourceEntryName(android.R.style.ButtonBar));
+ assertEquals("style", resources.getResourceTypeName(android.R.style.ButtonBar));
+ int id = resources.mLayoutlibCallback.getResourceId(ResourceType.STRING, "app_name");
+ assertEquals("com.android.layoutlib.test.myapplication:string/app_name",
+ resources.getResourceName(id));
+ assertEquals("com.android.layoutlib.test.myapplication",
+ resources.getResourcePackageName(id));
+ assertEquals("string", resources.getResourceTypeName(id));
+ assertEquals("app_name", resources.getResourceEntryName(id));
+ }
+
+ @Test
+ public void testStringEscaping() throws Exception {
+ // Setup
+ // Create the layout pull parser for our resources (empty.xml can not be part of the test
+ // app as it won't compile).
+ LayoutPullParser parser = new LayoutPullParser("/empty.xml");
+ // Create LayoutLibCallback.
+ LayoutLibTestCallback layoutLibCallback =
+ new LayoutLibTestCallback(RenderTestBase.getLogger(), mDefaultClassLoader);
+ layoutLibCallback.initResources();
+ SessionParams params = getSessionParams(parser, ConfigGenerator.NEXUS_4,
+ layoutLibCallback, "AppTheme", true, RenderingMode.NORMAL, 22);
+ AssetManager assetManager = AssetManager.getSystem();
+ DisplayMetrics metrics = new DisplayMetrics();
+ Configuration configuration = RenderAction.getConfiguration(params);
+ Resources resources = new Resources(assetManager, metrics, configuration);
+ resources.mLayoutlibCallback = params.getLayoutlibCallback();
+ resources.mContext =
+ new BridgeContext(params.getProjectKey(), metrics, params.getResources(),
+ params.getAssets(), params.getLayoutlibCallback(), configuration,
+ params.getTargetSdkVersion(), params.isRtlSupported());
+
+ int id = resources.mLayoutlibCallback.getResourceId(ResourceType.ARRAY, "string_array");
+ String[] strings = resources.getStringArray(id);
+ assertArrayEquals(
+ new String[]{"mystring", "Hello world!", "candidates", "Unknown", "?EC"},
+ strings);
+ assertTrue(sRenderMessages.isEmpty());
+ }
+}
diff --git a/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/ImageUtils.java b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/util/ImageUtils.java
similarity index 99%
rename from tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/ImageUtils.java
rename to tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/util/ImageUtils.java
index d3f0f89..18c6629 100644
--- a/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/ImageUtils.java
+++ b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/util/ImageUtils.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2014 The Android Open Source Project
+ * Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.layoutlib.bridge.intensive;
+package com.android.layoutlib.bridge.intensive.util;
import android.annotation.NonNull;
diff --git a/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/ModuleClassLoader.java b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/util/ModuleClassLoader.java
similarity index 97%
rename from tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/ModuleClassLoader.java
rename to tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/util/ModuleClassLoader.java
index 3fac778..da360f3 100644
--- a/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/ModuleClassLoader.java
+++ b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/util/ModuleClassLoader.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.layoutlib.bridge.intensive;
+package com.android.layoutlib.bridge.intensive.util;
import java.io.IOException;
import java.util.HashMap;
diff --git a/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/util/TestUtils.java b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/util/TestUtils.java
new file mode 100644
index 0000000..1df8e79
--- /dev/null
+++ b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/util/TestUtils.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2016 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.layoutlib.bridge.intensive.util;
+
+import java.lang.ref.WeakReference;
+
+public class TestUtils {
+ public static void gc() {
+ // See RuntimeUtil#gc in jlibs (http://jlibs.in/)
+ Object obj = new Object();
+ WeakReference ref = new WeakReference<>(obj);
+ //noinspection UnusedAssignment
+ obj = null;
+ while (ref.get() != null) {
+ System.gc();
+ System.runFinalization();
+ }
+
+ System.gc();
+ System.runFinalization();
+ }
+}
diff --git a/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/util/perf/LongStatsCollector.java b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/util/perf/LongStatsCollector.java
new file mode 100644
index 0000000..ee98b4b
--- /dev/null
+++ b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/util/perf/LongStatsCollector.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2016 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.layoutlib.bridge.intensive.util.perf;
+
+import android.annotation.NonNull;
+import android.util.LongArray;
+
+import java.util.Arrays;
+import java.util.function.LongConsumer;
+
+/**
+ * Class that collect a series of longs and produces the median, min and max values.
+ */
+public class LongStatsCollector implements LongConsumer {
+ private final LongArray mAllValues;
+ private long mMin = Long.MAX_VALUE;
+ private long mMax = Long.MIN_VALUE;
+ public LongStatsCollector(int estimatedRuns) {
+ mAllValues = new LongArray(estimatedRuns);
+ }
+
+ public int size() {
+ return mAllValues.size();
+ }
+
+ @NonNull
+ public Stats getStats() {
+ if (mAllValues.size() == 0) {
+ throw new IndexOutOfBoundsException("No data");
+ }
+
+ double median;
+ int size = mAllValues.size();
+ long[] buffer = new long[size];
+ for (int i = 0; i < size; i++) {
+ buffer[i] = mAllValues.get(i);
+ }
+
+ Arrays.sort(buffer);
+
+ int midPoint = size / 2;
+ median = (size % 2 == 0) ? (buffer[midPoint - 1] + buffer[midPoint]) / 2 : buffer[midPoint];
+
+ return new Stats(mAllValues.size(), mMin, mMax, median);
+ }
+
+ @Override
+ public void accept(long value) {
+ mMin = Math.min(mMin, value);
+ mMax = Math.max(mMax, value);
+ mAllValues.add(value);
+ }
+
+ public static class Stats {
+ private final int mSamples;
+ private final long mMin;
+ private final long mMax;
+ private final double mMedian;
+
+ private Stats(int samples, long min, long max, double median) {
+ mSamples = samples;
+ mMin = min;
+ mMax = max;
+ mMedian = median;
+ }
+
+ public int getSampleCount() {
+ return mSamples;
+ }
+
+ public long getMin() {
+ return mMin;
+ }
+
+ public long getMax() {
+ return mMax;
+ }
+
+ public double getMedian() {
+ return mMedian;
+ }
+ }
+}
diff --git a/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/util/perf/PerformanceRunner.java b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/util/perf/PerformanceRunner.java
new file mode 100644
index 0000000..7225a10
--- /dev/null
+++ b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/util/perf/PerformanceRunner.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2016 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.layoutlib.bridge.intensive.util.perf;
+
+import org.junit.runner.Runner;
+import org.junit.runner.notification.RunNotifier;
+import org.junit.runners.BlockJUnit4ClassRunner;
+import org.junit.runners.model.FrameworkMethod;
+import org.junit.runners.model.InitializationError;
+import org.junit.runners.model.Statement;
+
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * JUnit {@link Runner} that times the test execution and produces some stats.
+ */
+public class PerformanceRunner extends BlockJUnit4ClassRunner {
+ private static final int DEFAULT_WARMUP_ITERATIONS = 50;
+ private static final int DEFAULT_RUNS = 100;
+
+ private final int mWarmUpIterations;
+ private final int mRuns;
+
+ public PerformanceRunner(Class<?> testClass) throws InitializationError {
+ super(testClass);
+
+ Configuration classConfig = testClass.getAnnotation(Configuration.class);
+ mWarmUpIterations = classConfig != null && classConfig.warmUpIterations() != -1 ?
+ classConfig.warmUpIterations() :
+ DEFAULT_WARMUP_ITERATIONS;
+ mRuns = classConfig != null && classConfig.runs() != -1 ?
+ classConfig.runs() :
+ DEFAULT_RUNS;
+ }
+
+ @Override
+ protected Statement methodInvoker(FrameworkMethod method, Object test) {
+ int warmUpIterations;
+ int runs;
+
+ Configuration methodConfig = method.getAnnotation(Configuration.class);
+ warmUpIterations = methodConfig != null && methodConfig.warmUpIterations() != -1 ?
+ methodConfig.warmUpIterations() :
+ mWarmUpIterations;
+ runs = methodConfig != null && methodConfig.runs() != -1 ?
+ methodConfig.runs() :
+ mRuns;
+ return new TimedStatement(super.methodInvoker(method, test), warmUpIterations, runs,
+ (result) -> System.out.println(result.toString()));
+ }
+
+ @Override
+ public void run(RunNotifier notifier) {
+ super.run(notifier);
+ }
+
+ @Retention(RetentionPolicy.RUNTIME)
+ @Inherited
+ public @interface Configuration {
+ int warmUpIterations() default -1;
+
+ int runs() default -1;
+ }
+}
diff --git a/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/util/perf/TimedStatement.java b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/util/perf/TimedStatement.java
new file mode 100644
index 0000000..77a2b0e
--- /dev/null
+++ b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/util/perf/TimedStatement.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2016 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.layoutlib.bridge.intensive.util.perf;
+
+import com.android.layoutlib.bridge.intensive.util.TestUtils;
+
+import org.junit.runners.model.Statement;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Random;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Consumer;
+
+import com.google.common.hash.HashCode;
+import com.google.common.hash.HashFunction;
+import com.google.common.hash.Hashing;
+
+/**
+ * JUnit {@link Statement} used to measure some statistics about the test method.
+ */
+public class TimedStatement extends Statement {
+ private static final int CALIBRATION_WARMUP_ITERATIONS = 50;
+ private static final int CALIBRATION_RUNS = 100;
+
+ private static boolean sIsCalibrated;
+ private static double sCalibrated;
+
+ private final Statement mStatement;
+ private final int mWarmUpIterations;
+ private final int mRuns;
+ private final Runtime mRuntime = Runtime.getRuntime();
+ private final Consumer<TimedStatementResult> mCallback;
+
+ TimedStatement(Statement statement, int warmUpIterations, int runs,
+ Consumer<TimedStatementResult> finishedCallback) {
+ mStatement = statement;
+ mWarmUpIterations = warmUpIterations;
+ mRuns = runs;
+ mCallback = finishedCallback;
+ }
+
+ /**
+ * The calibrate method tries to do some work that involves IO, memory allocations and some
+ * operations on the randomly generated data to calibrate the speed of the machine with
+ * something that resembles the execution of a test case.
+ */
+ private static void calibrateMethod() throws IOException {
+ File tmpFile = File.createTempFile("test", "file");
+ Random rnd = new Random();
+ HashFunction hashFunction = Hashing.sha512();
+ for (int i = 0; i < 5 + rnd.nextInt(5); i++) {
+ FileOutputStream stream = new FileOutputStream(tmpFile);
+ int bytes = 30000 + rnd.nextInt(60000);
+ byte[] buffer = new byte[bytes];
+
+ rnd.nextBytes(buffer);
+ byte acc = 0;
+ for (int j = 0; j < bytes; j++) {
+ acc += buffer[i];
+ }
+ buffer[0] = acc;
+ stream.write(buffer);
+ System.gc();
+ stream.close();
+ FileInputStream input = new FileInputStream(tmpFile);
+ byte[] readBuffer = new byte[bytes];
+ //noinspection ResultOfMethodCallIgnored
+ input.read(readBuffer);
+ buffer = readBuffer;
+ HashCode code1 = hashFunction.hashBytes(buffer);
+ Arrays.sort(buffer);
+ HashCode code2 = hashFunction.hashBytes(buffer);
+ input.close();
+
+ FileOutputStream hashStream = new FileOutputStream(tmpFile);
+ hashStream.write(code1.asBytes());
+ hashStream.write(code2.asBytes());
+ hashStream.close();
+ }
+ }
+
+ /**
+ * Runs the calibration process and sets the calibration measure in {@link #sCalibrated}
+ */
+ private static void doCalibration() throws IOException {
+ System.out.println("Calibrating ...");
+ TestUtils.gc();
+ for (int i = 0; i < CALIBRATION_WARMUP_ITERATIONS; i++) {
+ calibrateMethod();
+ }
+
+ LongStatsCollector stats = new LongStatsCollector(CALIBRATION_RUNS);
+ for (int i = 0; i < CALIBRATION_RUNS; i++) {
+ TestUtils.gc();
+ long start = System.currentTimeMillis();
+ calibrateMethod();
+ stats.accept(System.currentTimeMillis() - start);
+ }
+
+ sCalibrated = stats.getStats().getMedian();
+ sIsCalibrated = true;
+ System.out.printf(" DONE %fms\n", sCalibrated);
+ }
+
+ private long getUsedMemory() {
+ return mRuntime.totalMemory() - mRuntime.freeMemory();
+ }
+
+
+ @Override
+ public void evaluate() throws Throwable {
+ if (!sIsCalibrated) {
+ doCalibration();
+ }
+
+ for (int i = 0; i < mWarmUpIterations; i++) {
+ mStatement.evaluate();
+ }
+
+ LongStatsCollector timeStats = new LongStatsCollector(mRuns);
+ LongStatsCollector memoryUseStats = new LongStatsCollector(mRuns);
+ AtomicBoolean collectSamples = new AtomicBoolean(false);
+
+ ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
+ TestUtils.gc();
+ executorService.scheduleAtFixedRate(() -> {
+ if (!collectSamples.get()) {
+ return;
+ }
+ memoryUseStats.accept(getUsedMemory());
+ }, 0, 200, TimeUnit.MILLISECONDS);
+
+ try {
+ for (int i = 0; i < mRuns; i++) {
+ TestUtils.gc();
+ collectSamples.set(true);
+ long startTimeMs = System.currentTimeMillis();
+ mStatement.evaluate();
+ long stopTimeMs = System.currentTimeMillis();
+ collectSamples.set(true);
+ timeStats.accept(stopTimeMs - startTimeMs);
+
+ }
+ } finally {
+ executorService.shutdownNow();
+ }
+
+ TimedStatementResult result = new TimedStatementResult(
+ mWarmUpIterations,
+ mRuns,
+ sCalibrated,
+ timeStats.getStats(),
+ memoryUseStats.getStats());
+ mCallback.accept(result);
+ }
+
+}
diff --git a/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/util/perf/TimedStatementResult.java b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/util/perf/TimedStatementResult.java
new file mode 100644
index 0000000..59f90d2
--- /dev/null
+++ b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/util/perf/TimedStatementResult.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2016 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.layoutlib.bridge.intensive.util.perf;
+
+import com.android.layoutlib.bridge.intensive.util.perf.LongStatsCollector.Stats;
+
+import java.text.DecimalFormat;
+
+/**
+ * Result value of a {@link TimedStatement}
+ */
+public class TimedStatementResult {
+ private static final DecimalFormat UNITS_FORMAT = new DecimalFormat("#.##");
+
+ private final int mWarmUpIterations;
+ private final int mRuns;
+ private final double mCalibrationTimeMs;
+ private final Stats mTimeStats;
+ private final Stats mMemoryStats;
+
+ TimedStatementResult(int warmUpIterations, int runs,
+ double calibrationTimeMs,
+ Stats timeStats,
+ Stats memoryStats) {
+ mWarmUpIterations = warmUpIterations;
+ mRuns = runs;
+ mCalibrationTimeMs = calibrationTimeMs;
+ mTimeStats = timeStats;
+ mMemoryStats = memoryStats;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ "Warm up %d. Runs %d\n" + "Time: %s ms (min: %s, max %s)\n" +
+ "Calibration Time: %f ms\n" +
+ "Calibrated Time: %s units (min: %s, max %s)\n" +
+ "Sampled %d times\n" +
+ " Memory used: %d bytes (max %d)\n\n",
+ mWarmUpIterations, mRuns,
+ mTimeStats.getMedian(), mTimeStats.getMin(), mTimeStats.getMax(),
+ mCalibrationTimeMs,
+ UNITS_FORMAT.format((mTimeStats.getMedian() / mCalibrationTimeMs) * 100000),
+ UNITS_FORMAT.format((mTimeStats.getMin() / mCalibrationTimeMs) * 100000),
+ UNITS_FORMAT.format((mTimeStats.getMax() / mCalibrationTimeMs) * 100000),
+ mMemoryStats.getSampleCount(),
+ (long)mMemoryStats.getMedian() - mMemoryStats.getMin(),
+ mMemoryStats.getMax() - mMemoryStats.getMin());
+ }
+}
diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/CreateInfo.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/CreateInfo.java
index b6aaac8..741eb27 100644
--- a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/CreateInfo.java
+++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/CreateInfo.java
@@ -154,6 +154,8 @@
"android.content.res.Resources#getIntArray",
"android.content.res.Resources#getInteger",
"android.content.res.Resources#getLayout",
+ "android.content.res.Resources#getQuantityString",
+ "android.content.res.Resources#getQuantityText",
"android.content.res.Resources#getResourceEntryName",
"android.content.res.Resources#getResourceName",
"android.content.res.Resources#getResourcePackageName",
@@ -232,6 +234,7 @@
"android.view.RenderNode#nSetScaleY",
"android.view.RenderNode#nGetScaleY",
"android.view.RenderNode#nIsPivotExplicitlySet",
+ "android.view.PointerIcon#loadResource",
"android.view.ViewGroup#drawChild",
"com.android.internal.view.menu.MenuBuilder#createNewMenuItem",
"com.android.internal.util.XmlUtils#convertValueToInt",