Merge "Fix bug where mExtensions was not initialized."
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index 8bf8dcb..faed7a0 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -122,7 +122,10 @@
* Provisioning adds a managed profile and sets the MDM as the profile owner who has full
* control over the profile.
*
- * In version {@link android.os.Build.VERSION_CODES#LOLLIPOP}, this intent must contain the
+ * <p>It is possible to check if provisioning is allowed or not by querying the method
+ * {@link #isProvisioningAllowed(String)}.
+ *
+ * <p>In version {@link android.os.Build.VERSION_CODES#LOLLIPOP}, this intent must contain the
* extra {@link #EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_NAME}.
* As of {@link android.os.Build.VERSION_CODES#M}, it should contain the extra
* {@link #EXTRA_PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME} instead, although specifying only
@@ -157,9 +160,8 @@
* been completed. Use {@link #isProvisioningAllowed(String)} to check if provisioning is
* allowed.
*
- * This intent should contain the extra {@link #EXTRA_PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME},
- * although specifying only {@link #EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_NAME} is also
- * supported.
+ * <p>This intent should contain the extra
+ * {@link #EXTRA_PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME}.
*
* <p> If provisioning fails, the device returns to its previous state.
*
@@ -185,10 +187,10 @@
* employee or client.
*
* <p> An intent with this action can be sent only on an unprovisioned device.
- * It is possible to check if the device is provisioned or not by looking at
- * {@link android.provider.Settings.Global#DEVICE_PROVISIONED}
+ * It is possible to check if provisioning is allowed or not by querying the method
+ * {@link #isProvisioningAllowed(String)}.
*
- * The intent contains the following extras:
+ * <p>The intent contains the following extras:
* <ul>
* <li>{@link #EXTRA_PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME}</li>
* <li>{@link #EXTRA_PROVISIONING_SKIP_ENCRYPTION}, optional</li>
@@ -212,6 +214,53 @@
= "android.app.action.PROVISION_MANAGED_DEVICE";
/**
+ * Activity action: Starts the provisioning flow which sets up a managed device.
+ * Must be started with {@link android.app.Activity#startActivityForResult(Intent, int)}.
+ *
+ * <p>NOTE: This is only supported on split system user devices, and puts the device into a
+ * management state that is distinct from that reached by
+ * {@link #ACTION_PROVISION_MANAGED_DEVICE} - specifically the device owner runs on the system
+ * user, and only has control over device-wide policies, not individual users and their data.
+ * The primary benefit is that multiple non-system users are supported when provisioning using
+ * this form of device management.
+ *
+ * <p> During device owner provisioning a device admin app is set as the owner of the device.
+ * A device owner has full control over the device. The device owner can not be modified by the
+ * user.
+ *
+ * <p> A typical use case would be a device that is owned by a company, but used by either an
+ * employee or client.
+ *
+ * <p> An intent with this action can be sent only on an unprovisioned device.
+ * It is possible to check if provisioning is allowed or not by querying the method
+ * {@link #isProvisioningAllowed(String)}.
+ *
+ * <p>The intent contains the following extras:
+ * <ul>
+ * <li>{@link #EXTRA_PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME}</li>
+ * <li>{@link #EXTRA_PROVISIONING_SKIP_ENCRYPTION}, optional</li>
+ * <li>{@link #EXTRA_PROVISIONING_LEAVE_ALL_SYSTEM_APPS_ENABLED}, optional</li>
+ * <li>{@link #EXTRA_PROVISIONING_ADMIN_EXTRAS_BUNDLE}, optional</li>
+ * </ul>
+ *
+ * <p> When device owner provisioning has completed, an intent of the type
+ * {@link DeviceAdminReceiver#ACTION_PROFILE_PROVISIONING_COMPLETE} is broadcast to the
+ * device owner.
+ *
+ * <p> If provisioning fails, the device is factory reset.
+ *
+ * <p>A result code of {@link android.app.Activity#RESULT_OK} implies that the synchronous part
+ * of the provisioning flow was successful, although this doesn't guarantee the full flow will
+ * succeed. Conversely a result code of {@link android.app.Activity#RESULT_CANCELED} implies
+ * that the user backed-out of provisioning, or some precondition for provisioning wasn't met.
+ *
+ * @hide
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_PROVISION_MANAGED_SHAREABLE_DEVICE
+ = "android.app.action.PROVISION_MANAGED_SHAREABLE_DEVICE";
+
+ /**
* A {@link android.os.Parcelable} extra of type {@link android.os.PersistableBundle} that
* allows a mobile device management application or NFC programmer application which starts
* managed provisioning to pass data to the management application instance after provisioning.
@@ -1662,7 +1711,16 @@
* Force a new device unlock password (the password needed to access the
* entire device, not for individual accounts) on the user. This takes
* effect immediately.
- * The given password must be sufficient for the
+ *
+ * <p>Calling this from a managed profile that shares the password with the owner profile
+ * will throw a security exception.
+ *
+ * <p><em>Note: This API has been limited as of {@link android.os.Build.VERSION_CODES#N} for
+ * device admins that are not device owner and not profile owner.
+ * The password can now only be changed if there is currently no password set. Device owner
+ * and profile owner can still do this.</em>
+ *
+ * <p>The given password must be sufficient for the
* current password quality and length constraints as returned by
* {@link #getPasswordQuality(ComponentName)} and
* {@link #getPasswordMinimumLength(ComponentName)}; if it does not meet
@@ -1672,19 +1730,20 @@
* the currently active quality will be increased to match.
*
* <p>Calling with a null or empty password will clear any existing PIN,
- * pattern or password if the current password constraints allow it.
+ * pattern or password if the current password constraints allow it. <em>Note: This will not
+ * work in {@link android.os.Build.VERSION_CODES#N} and later for device admins that are not
+ * device owner and not profile owner. Once set, the password cannot be changed to null or
+ * empty, except by device owner or profile owner.</em>
*
* <p>The calling device admin must have requested
* {@link DeviceAdminInfo#USES_POLICY_RESET_PASSWORD} to be able to call
* this method; if it has not, a security exception will be thrown.
*
- * <p>Calling this from a managed profile will throw a security exception.
- *
* @param password The new password for the user. Null or empty clears the password.
* @param flags May be 0 or combination of {@link #RESET_PASSWORD_REQUIRE_ENTRY} and
* {@link #RESET_PASSWORD_DO_NOT_ASK_CREDENTIALS_ON_BOOT}.
* @return Returns true if the password was applied, or false if it is
- * not acceptable for the current constraints.
+ * not acceptable for the current constraints or if the user has not been decrypted yet.
*/
public boolean resetPassword(String password, int flags) {
if (mService != null) {
@@ -1792,7 +1851,7 @@
public void wipeData(int flags) {
if (mService != null) {
try {
- mService.wipeData(flags, myUserId());
+ mService.wipeData(flags);
} catch (RemoteException e) {
Log.w(TAG, "Failed talking with device policy service", e);
}
@@ -2668,14 +2727,14 @@
* does *not* check weather the device owner is actually running on the current user.
*/
public boolean isDeviceOwnerApp(String packageName) {
- if (mService != null) {
- try {
- return mService.isDeviceOwnerPackage(packageName);
- } catch (RemoteException e) {
- Log.w(TAG, "Failed talking with device policy service", e);
- }
+ if (packageName == null) {
+ return false;
}
- return false;
+ final ComponentName deviceOwner = getDeviceOwnerComponent();
+ if (deviceOwner == null) {
+ return false;
+ }
+ return packageName.equals(deviceOwner.getPackageName());
}
/**
diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl
index e7e1833..7601cf2 100644
--- a/core/java/android/app/admin/IDevicePolicyManager.aidl
+++ b/core/java/android/app/admin/IDevicePolicyManager.aidl
@@ -81,7 +81,7 @@
void lockNow();
- void wipeData(int flags, int userHandle);
+ void wipeData(int flags);
ComponentName setGlobalProxy(in ComponentName admin, String proxySpec, String exclusionList);
ComponentName getGlobalProxyAdmin(int userHandle);
@@ -114,7 +114,6 @@
void reportSuccessfulPasswordAttempt(int userHandle);
boolean setDeviceOwner(in ComponentName who, String ownerName, int userId);
- boolean isDeviceOwnerPackage(String packageName);
ComponentName getDeviceOwner();
String getDeviceOwnerName();
void clearDeviceOwner(String packageName);
diff --git a/core/java/android/content/pm/AppsQueryHelper.java b/core/java/android/content/pm/AppsQueryHelper.java
index a5a8e3f..084bc18 100644
--- a/core/java/android/content/pm/AppsQueryHelper.java
+++ b/core/java/android/content/pm/AppsQueryHelper.java
@@ -18,13 +18,11 @@
import android.Manifest;
import android.app.AppGlobals;
-import android.content.Context;
import android.content.Intent;
import android.os.RemoteException;
import android.os.UserHandle;
import android.util.ArraySet;
-import android.view.inputmethod.InputMethodInfo;
-import android.view.inputmethod.InputMethodManager;
+import android.view.inputmethod.InputMethod;
import com.android.internal.annotations.VisibleForTesting;
@@ -44,36 +42,37 @@
public static int GET_NON_LAUNCHABLE_APPS = 1;
/**
- * Return apps with {@link android.Manifest.permission#INTERACT_ACROSS_USERS} permission
+ * Return apps with {@link Manifest.permission#INTERACT_ACROSS_USERS} permission
*/
public static int GET_APPS_WITH_INTERACT_ACROSS_USERS_PERM = 1 << 1;
/**
- * Return all input methods that are marked as default.
- * <p>When this flag is set, {@code user} specified in
- * {@link #queryApps(int, boolean, UserHandle)} must be
- * {@link UserHandle#myUserId user of the current process}.
+ * Return all input methods available for the current user.
*/
- public static int GET_DEFAULT_IMES = 1 << 2;
+ public static int GET_IMES = 1 << 2;
- private final Context mContext;
+ private final IPackageManager mPackageManager;
private List<ApplicationInfo> mAllApps;
- public AppsQueryHelper(Context context) {
- mContext = context;
+ public AppsQueryHelper(IPackageManager packageManager) {
+ mPackageManager = packageManager;
+ }
+
+ public AppsQueryHelper() {
+ this(AppGlobals.getPackageManager());
}
/**
* Return a List of all packages that satisfy a specified criteria.
* @param flags search flags. Use any combination of {@link #GET_NON_LAUNCHABLE_APPS},
- * {@link #GET_APPS_WITH_INTERACT_ACROSS_USERS_PERM} or {@link #GET_DEFAULT_IMES}.
+ * {@link #GET_APPS_WITH_INTERACT_ACROSS_USERS_PERM} or {@link #GET_IMES}.
* @param systemAppsOnly if true, only system apps will be returned
* @param user user, whose apps are queried
*/
public List<String> queryApps(int flags, boolean systemAppsOnly, UserHandle user) {
boolean nonLaunchableApps = (flags & GET_NON_LAUNCHABLE_APPS) > 0;
boolean interactAcrossUsers = (flags & GET_APPS_WITH_INTERACT_ACROSS_USERS_PERM) > 0;
- boolean defaultImes = (flags & GET_DEFAULT_IMES) > 0;
+ boolean imes = (flags & GET_IMES) > 0;
if (mAllApps == null) {
mAllApps = getAllApps(user.getIdentifier());
}
@@ -113,7 +112,6 @@
}
}
}
-
if (interactAcrossUsers) {
final List<PackageInfo> packagesHoldingPermissions = getPackagesHoldingPermission(
Manifest.permission.INTERACT_ACROSS_USERS, user.getIdentifier());
@@ -129,29 +127,18 @@
}
}
- if (defaultImes) {
- if (UserHandle.myUserId() != user.getIdentifier()) {
- throw new IllegalArgumentException("Specified user handle " + user
- + " is not a user of the current process.");
- }
- List<InputMethodInfo> imis = getInputMethodList();
- int imisSize = imis.size();
- ArraySet<String> defaultImePackages = new ArraySet<>();
- for (int i = 0; i < imisSize; i++) {
- InputMethodInfo imi = imis.get(i);
- if (imi.isDefault(mContext)) {
- defaultImePackages.add(imi.getPackageName());
- }
- }
- final int allAppsSize = mAllApps.size();
- for (int i = 0; i < allAppsSize; i++) {
- final ApplicationInfo appInfo = mAllApps.get(i);
- if (systemAppsOnly && !appInfo.isSystemApp()) {
+ if (imes) {
+ final List<ResolveInfo> resolveInfos = queryIntentServicesAsUser(
+ new Intent(InputMethod.SERVICE_INTERFACE), user.getIdentifier());
+ final int resolveInfosSize = resolveInfos.size();
+
+ for (int i = 0; i < resolveInfosSize; i++) {
+ ServiceInfo serviceInfo = resolveInfos.get(i).serviceInfo;
+ if (systemAppsOnly && !serviceInfo.applicationInfo.isSystemApp()) {
continue;
}
- final String packageName = appInfo.packageName;
- if (defaultImePackages.contains(packageName)) {
- result.add(packageName);
+ if (!result.contains(serviceInfo.packageName)) {
+ result.add(serviceInfo.packageName);
}
}
}
@@ -163,9 +150,8 @@
@SuppressWarnings("unchecked")
protected List<ApplicationInfo> getAllApps(int userId) {
try {
- return AppGlobals.getPackageManager().getInstalledApplications(
- PackageManager.GET_UNINSTALLED_PACKAGES
- | PackageManager.GET_DISABLED_COMPONENTS, userId).getList();
+ return mPackageManager.getInstalledApplications(PackageManager.GET_UNINSTALLED_PACKAGES
+ | PackageManager.GET_DISABLED_COMPONENTS, userId).getList();
} catch (RemoteException e) {
throw new IllegalStateException("Package manager has died", e);
}
@@ -173,17 +159,22 @@
@VisibleForTesting
protected List<ResolveInfo> queryIntentActivitiesAsUser(Intent intent, int userId) {
- return mContext.getPackageManager()
- .queryIntentActivitiesAsUser(intent, PackageManager.GET_DISABLED_COMPONENTS
- | PackageManager.GET_UNINSTALLED_PACKAGES, userId);
+ try {
+ return mPackageManager.queryIntentActivities(intent, null,
+ PackageManager.GET_DISABLED_COMPONENTS
+ | PackageManager.GET_UNINSTALLED_PACKAGES,
+ userId);
+ } catch (RemoteException e) {
+ throw new IllegalStateException("Package manager has died", e);
+ }
}
@VisibleForTesting
- @SuppressWarnings("unchecked")
- protected List<PackageInfo> getPackagesHoldingPermission(String perm, int userId) {
+ protected List<ResolveInfo> queryIntentServicesAsUser(Intent intent, int userId) {
try {
- return AppGlobals.getPackageManager().getPackagesHoldingPermissions(new String[]{perm},
- 0, userId).getList();
+ return mPackageManager.queryIntentServices(intent, null,
+ PackageManager.GET_META_DATA
+ | PackageManager.GET_DISABLED_UNTIL_USED_COMPONENTS, userId);
} catch (RemoteException e) {
throw new IllegalStateException("Package manager has died", e);
}
@@ -191,9 +182,13 @@
@VisibleForTesting
@SuppressWarnings("unchecked")
- protected List<InputMethodInfo> getInputMethodList() {
- InputMethodManager imm = (InputMethodManager)
- mContext.getSystemService(Context.INPUT_METHOD_SERVICE);
- return imm.getInputMethodList();
+ protected List<PackageInfo> getPackagesHoldingPermission(String perm, int userId) {
+ try {
+ return mPackageManager.getPackagesHoldingPermissions(new String[]{perm}, 0,
+ userId).getList();
+ } catch (RemoteException e) {
+ throw new IllegalStateException("Package manager has died", e);
+ }
}
+
}
diff --git a/core/java/android/net/http/X509TrustManagerExtensions.java b/core/java/android/net/http/X509TrustManagerExtensions.java
index 25ef8b5..6729347 100644
--- a/core/java/android/net/http/X509TrustManagerExtensions.java
+++ b/core/java/android/net/http/X509TrustManagerExtensions.java
@@ -20,6 +20,9 @@
import com.android.org.conscrypt.TrustManagerImpl;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.List;
@@ -36,7 +39,11 @@
*/
public class X509TrustManagerExtensions {
- final TrustManagerImpl mDelegate;
+ private final TrustManagerImpl mDelegate;
+ // Methods to use when mDelegate is not a TrustManagerImpl and duck typing is being used.
+ private final X509TrustManager mTrustManager;
+ private final Method mCheckServerTrusted;
+ private final Method mIsUserAddedCertificate;
/**
* Constructs a new X509TrustManagerExtensions wrapper.
@@ -47,10 +54,31 @@
public X509TrustManagerExtensions(X509TrustManager tm) throws IllegalArgumentException {
if (tm instanceof TrustManagerImpl) {
mDelegate = (TrustManagerImpl) tm;
- } else {
- mDelegate = null;
- throw new IllegalArgumentException("tm is an instance of " + tm.getClass().getName() +
- " which is not a supported type of X509TrustManager");
+ mTrustManager = null;
+ mCheckServerTrusted = null;
+ mIsUserAddedCertificate = null;
+ return;
+ }
+ // Use duck typing if possible.
+ mDelegate = null;
+ mTrustManager = tm;
+ // Check that the hostname aware checkServerTrusted is present.
+ try {
+ mCheckServerTrusted = tm.getClass().getMethod("checkServerTrusted",
+ X509Certificate[].class,
+ String.class,
+ String.class);
+ } catch (NoSuchMethodException e) {
+ throw new IllegalArgumentException("Required method"
+ + " checkServerTrusted(X509Certificate[], String, String, String) missing");
+ }
+ // Check that isUserAddedCertificate is present.
+ try {
+ mIsUserAddedCertificate = tm.getClass().getMethod("isUserAddedCertificate",
+ X509Certificate.class);
+ } catch (NoSuchMethodException e) {
+ throw new IllegalArgumentException(
+ "Required method isUserAddedCertificate(X509Certificate) missing");
}
}
@@ -66,7 +94,24 @@
*/
public List<X509Certificate> checkServerTrusted(X509Certificate[] chain, String authType,
String host) throws CertificateException {
- return mDelegate.checkServerTrusted(chain, authType, host);
+ if (mDelegate != null) {
+ return mDelegate.checkServerTrusted(chain, authType, host);
+ } else {
+ try {
+ return (List<X509Certificate>) mCheckServerTrusted.invoke(mTrustManager, chain,
+ authType, host);
+ } catch (IllegalAccessException e) {
+ throw new CertificateException("Failed to call checkServerTrusted", e);
+ } catch (InvocationTargetException e) {
+ if (e.getCause() instanceof CertificateException) {
+ throw (CertificateException) e.getCause();
+ }
+ if (e.getCause() instanceof RuntimeException) {
+ throw (RuntimeException) e.getCause();
+ }
+ throw new CertificateException("checkServerTrusted failed", e.getCause());
+ }
+ }
}
/**
@@ -80,7 +125,21 @@
* otherwise.
*/
public boolean isUserAddedCertificate(X509Certificate cert) {
- return mDelegate.isUserAddedCertificate(cert);
+ if (mDelegate != null) {
+ return mDelegate.isUserAddedCertificate(cert);
+ } else {
+ try {
+ return (Boolean) mIsUserAddedCertificate.invoke(mTrustManager, cert);
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException("Failed to call isUserAddedCertificate", e);
+ } catch (InvocationTargetException e) {
+ if (e.getCause() instanceof RuntimeException) {
+ throw (RuntimeException) e.getCause();
+ } else {
+ throw new RuntimeException("isUserAddedCertificate failed", e.getCause());
+ }
+ }
+ }
}
/**
diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java
index 5852f5f..f2aea08 100644
--- a/core/java/android/os/Parcel.java
+++ b/core/java/android/os/Parcel.java
@@ -624,6 +624,32 @@
}
/**
+ * {@hide}
+ * This will be the new name for writeFileDescriptor, for consistency.
+ **/
+ public final void writeRawFileDescriptor(FileDescriptor val) {
+ nativeWriteFileDescriptor(mNativePtr, val);
+ }
+
+ /**
+ * {@hide}
+ * Write an array of FileDescriptor objects into the Parcel.
+ *
+ * @param value The array of objects to be written.
+ */
+ public final void writeRawFileDescriptorArray(FileDescriptor[] value) {
+ if (value != null) {
+ int N = value.length;
+ writeInt(N);
+ for (int i=0; i<N; i++) {
+ writeRawFileDescriptor(value[i]);
+ }
+ } else {
+ writeInt(-1);
+ }
+ }
+
+ /**
* Write a byte value into the parcel at the current dataPosition(),
* growing dataCapacity() if needed.
*/
@@ -1700,6 +1726,41 @@
return nativeReadFileDescriptor(mNativePtr);
}
+ /**
+ * {@hide}
+ * Read and return a new array of FileDescriptors from the parcel.
+ * @return the FileDescriptor array, or null if the array is null.
+ **/
+ public final FileDescriptor[] createRawFileDescriptorArray() {
+ int N = readInt();
+ if (N < 0) {
+ return null;
+ }
+ FileDescriptor[] f = new FileDescriptor[N];
+ for (int i = 0; i < N; i++) {
+ f[i] = readRawFileDescriptor();
+ }
+ return f;
+ }
+
+ /**
+ * {@hide}
+ * Read an array of FileDescriptors from a parcel.
+ * The passed array must be exactly the length of the array in the parcel.
+ * @return the FileDescriptor array, or null if the array is null.
+ **/
+ public final void readRawFileDescriptorArray(FileDescriptor[] val) {
+ int N = readInt();
+ if (N == val.length) {
+ for (int i=0; i<N; i++) {
+ val[i] = readRawFileDescriptor();
+ }
+ } else {
+ throw new RuntimeException("bad array lengths");
+ }
+ }
+
+
/*package*/ static native FileDescriptor openFileDescriptor(String file,
int mode) throws FileNotFoundException;
/*package*/ static native FileDescriptor dupFileDescriptor(FileDescriptor orig)
diff --git a/core/java/android/security/net/config/NetworkSecurityConfig.java b/core/java/android/security/net/config/NetworkSecurityConfig.java
index 503854e..8906f9b 100644
--- a/core/java/android/security/net/config/NetworkSecurityConfig.java
+++ b/core/java/android/security/net/config/NetworkSecurityConfig.java
@@ -41,7 +41,7 @@
private final List<CertificatesEntryRef> mCertificatesEntryRefs;
private Set<TrustAnchor> mAnchors;
private final Object mAnchorsLock = new Object();
- private X509TrustManager mTrustManager;
+ private NetworkSecurityTrustManager mTrustManager;
private final Object mTrustManagerLock = new Object();
private NetworkSecurityConfig(boolean cleartextTrafficPermitted, boolean hstsEnforced,
@@ -78,7 +78,7 @@
return mPins;
}
- public X509TrustManager getTrustManager() {
+ public NetworkSecurityTrustManager getTrustManager() {
synchronized(mTrustManagerLock) {
if (mTrustManager == null) {
mTrustManager = new NetworkSecurityTrustManager(this);
diff --git a/core/java/android/security/net/config/NetworkSecurityTrustManager.java b/core/java/android/security/net/config/NetworkSecurityTrustManager.java
index e69082d..7f5b3ca 100644
--- a/core/java/android/security/net/config/NetworkSecurityTrustManager.java
+++ b/core/java/android/security/net/config/NetworkSecurityTrustManager.java
@@ -71,9 +71,28 @@
@Override
public void checkServerTrusted(X509Certificate[] certs, String authType)
throws CertificateException {
- List<X509Certificate> trustedChain =
- mDelegate.checkServerTrusted(certs, authType, (String) null);
+ checkServerTrusted(certs, authType, null);
+ }
+
+ /**
+ * Hostname aware version of {@link #checkServerTrusted(X509Certificate[], String)}.
+ * This interface is used by conscrypt and android.net.http.X509TrustManagerExtensions do not
+ * modify without modifying those callers.
+ */
+ public List<X509Certificate> checkServerTrusted(X509Certificate[] certs, String authType,
+ String host) throws CertificateException {
+ List<X509Certificate> trustedChain = mDelegate.checkServerTrusted(certs, authType, host);
checkPins(trustedChain);
+ return trustedChain;
+ }
+
+ /**
+ * Check if the provided certificate is a user added certificate authority.
+ * This is required by android.net.http.X509TrustManagerExtensions.
+ */
+ public boolean isUserAddedCertificate(X509Certificate cert) {
+ // TODO: Figure out the right way to handle this, and if it is still even used.
+ return false;
}
private void checkPins(List<X509Certificate> chain) throws CertificateException {
diff --git a/core/java/android/security/net/config/RootTrustManager.java b/core/java/android/security/net/config/RootTrustManager.java
index 1338b9f..b87bf1f 100644
--- a/core/java/android/security/net/config/RootTrustManager.java
+++ b/core/java/android/security/net/config/RootTrustManager.java
@@ -18,6 +18,7 @@
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
+import java.util.List;
import javax.net.ssl.X509TrustManager;
@@ -61,10 +62,24 @@
config.getTrustManager().checkServerTrusted(certs, authType);
}
- public void checkServerTrusted(X509Certificate[] certs, String authType, String hostname)
- throws CertificateException {
+ /**
+ * Hostname aware version of {@link #checkServerTrusted(X509Certificate[], String)}.
+ * This interface is used by conscrypt and android.net.http.X509TrustManagerExtensions do not
+ * modify without modifying those callers.
+ */
+ public List<X509Certificate> checkServerTrusted(X509Certificate[] certs, String authType,
+ String hostname) throws CertificateException {
NetworkSecurityConfig config = mConfig.getConfigForHostname(hostname);
- config.getTrustManager().checkServerTrusted(certs, authType);
+ return config.getTrustManager().checkServerTrusted(certs, authType, hostname);
+ }
+
+ /**
+ * Check if the provided certificate is a user added certificate authority.
+ * This is required by android.net.http.X509TrustManagerExtensions.
+ */
+ public boolean isUserAddedCertificate(X509Certificate cert) {
+ // TODO: Figure out the right way to handle this, and if it is still even used.
+ return false;
}
@Override
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 66b05a2..461506b 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -12968,10 +12968,6 @@
mPrivateFlags |= PFLAG_DIRTY;
- // Release any resources in-case we don't end up drawing again
- // as anything cached is no longer valid
- resetDisplayList();
-
if (invalidateCache) {
mPrivateFlags |= PFLAG_INVALIDATED;
mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 625ed38..fdea1ba 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -1638,6 +1638,8 @@
final boolean stableInsetsChanged = !mPendingStableInsets.equals(
mAttachInfo.mStableInsets);
final boolean outsetsChanged = !mPendingOutsets.equals(mAttachInfo.mOutsets);
+ final boolean surfaceSizeChanged = (relayoutResult
+ & WindowManagerGlobal.RELAYOUT_RES_SURFACE_RESIZED) != 0;
if (contentInsetsChanged) {
mAttachInfo.mContentInsets.set(mPendingContentInsets);
if (DEBUG_LAYOUT) Log.v(TAG, "Content insets changing to: "
@@ -1723,10 +1725,20 @@
mAttachInfo.mHardwareRenderer.isEnabled()) {
mAttachInfo.mHardwareRenderer.destroy();
}
- } else if (surfaceGenerationId != mSurface.getGenerationId() &&
- mSurfaceHolder == null && mAttachInfo.mHardwareRenderer != null) {
+ } else if ((surfaceGenerationId != mSurface.getGenerationId()
+ || surfaceSizeChanged)
+ && mSurfaceHolder == null
+ && mAttachInfo.mHardwareRenderer != null) {
mFullRedrawNeeded = true;
try {
+ // Need to do updateSurface (which leads to CanvasContext::setSurface and
+ // re-create the EGLSurface) if either the Surface changed (as indicated by
+ // generation id), or WindowManager changed the surface size. The latter is
+ // because on some chips, changing the consumer side's BufferQueue size may
+ // not take effect immediately unless we create a new EGLSurface.
+ // Note that frame size change doesn't always imply surface size change (eg.
+ // drag resizing uses fullscreen surface), need to check surfaceSizeChanged
+ // flag from WindowManager.
mAttachInfo.mHardwareRenderer.updateSurface(mSurface);
} catch (OutOfResourcesException e) {
handleOutOfResourcesException(e);
diff --git a/core/java/android/view/WindowManagerGlobal.java b/core/java/android/view/WindowManagerGlobal.java
index 43d643e..c08e1b5 100644
--- a/core/java/android/view/WindowManagerGlobal.java
+++ b/core/java/android/view/WindowManagerGlobal.java
@@ -77,6 +77,11 @@
public static final int RELAYOUT_RES_DRAG_RESIZING = 0x8;
/**
+ * The window manager has changed the size of the surface from the last call.
+ */
+ public static final int RELAYOUT_RES_SURFACE_RESIZED = 0x10;
+
+ /**
* Flag for relayout: the client will be later giving
* internal insets; as a result, the window will not impact other window
* layouts until the insets are given.
diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java
index ee06806..122b83a 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -443,30 +443,18 @@
if (DEBUG) {
Log.i(TAG, "handleMessage: MSG_UNBIND " + sequence);
}
- boolean startInput = false;
+ final boolean startInput;
synchronized (mH) {
- if (mBindSequence == sequence) {
- if (false) {
- // XXX the server has already unbound!
- if (mCurMethod != null && mCurrentTextBoxAttribute != null) {
- try {
- mCurMethod.finishInput();
- } catch (RemoteException e) {
- Log.w(TAG, "IME died: " + mCurId, e);
- }
- }
- }
- clearBindingLocked();
-
- // If we were actively using the last input method, then
- // we would like to re-connect to the next input method.
- if (mServedView != null && mServedView.isFocused()) {
- mServedConnecting = true;
- }
- if (mActive) {
- startInput = true;
- }
+ if (mBindSequence != sequence) {
+ return;
}
+ clearBindingLocked();
+ // If we were actively using the last input method, then
+ // we would like to re-connect to the next input method.
+ if (mServedView != null && mServedView.isFocused()) {
+ mServedConnecting = true;
+ }
+ startInput = mActive;
}
if (startInput) {
startInputInner(null, 0, 0, 0);
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index c54a574..b5d994d 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -3975,6 +3975,15 @@
}
((Editable) mText).append(text, start, end);
+
+ if (mAutoLinkMask != 0) {
+ boolean linksWereAdded = Linkify.addLinks((Spannable) mText, mAutoLinkMask);
+ // Do not change the movement method for text that support text selection as it
+ // would prevent an arbitrary cursor displacement.
+ if (linksWereAdded && mLinksClickable && !textCanBeSelected()) {
+ setMovementMethod(LinkMovementMethod.getInstance());
+ }
+ }
}
private void updateTextColors() {
diff --git a/core/jni/android_util_Log.cpp b/core/jni/android_util_Log.cpp
index 2d23cda..c89f293c 100644
--- a/core/jni/android_util_Log.cpp
+++ b/core/jni/android_util_Log.cpp
@@ -42,7 +42,9 @@
static levels_t levels;
static jboolean isLoggable(const char* tag, jint level) {
- return __android_log_is_loggable(level, tag, ANDROID_LOG_INFO);
+ return __android_log_is_loggable(level, tag,
+ ANDROID_LOG_INFO |
+ ANDROID_LOGGABLE_FLAG_NOT_WITHIN_SIGNAL);
}
static jboolean android_util_Log_isLoggable(JNIEnv* env, jobject clazz, jstring tag, jint level)
diff --git a/core/jni/android_util_PathParser.cpp b/core/jni/android_util_PathParser.cpp
index bf6ffc54..245aa0f 100644
--- a/core/jni/android_util_PathParser.cpp
+++ b/core/jni/android_util_PathParser.cpp
@@ -19,6 +19,7 @@
#include <PathParser.h>
#include <SkPath.h>
+#include <android/log.h>
#include "core_jni_helpers.h"
namespace android {
@@ -27,10 +28,14 @@
jint strLength) {
const char* pathString = env->GetStringUTFChars(inputPathStr, NULL);
SkPath* skPath = reinterpret_cast<SkPath*>(skPathHandle);
- bool hasValidData = android::uirenderer::PathParser::parseStringForSkPath(skPath, pathString,
- strLength);
+
+ android::uirenderer::PathParser::ParseResult result;
+ android::uirenderer::PathParser::parseStringForSkPath(skPath, &result, pathString, strLength);
env->ReleaseStringUTFChars(inputPathStr, pathString);
- return hasValidData;
+ if (result.failureOccurred) {
+ ALOGE(result.failureMessage.c_str());
+ }
+ return !result.failureOccurred;
}
static const JNINativeMethod gMethods[] = {
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 9c75b7a..b87d9e2 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -411,7 +411,7 @@
<bool translatable="false" name="config_wifi_revert_country_code_on_cellular_loss">false</bool>
<!-- Boolean indicating whether or not wifi firmware debugging is enabled -->
- <bool translatable="false" name="config_wifi_enable_wifi_firmware_debugging">false</bool>
+ <bool translatable="false" name="config_wifi_enable_wifi_firmware_debugging">true</bool>
<!-- Integer specifying the basic autojoin parameters -->
<integer translatable="false" name="config_wifi_framework_5GHz_preference_boost_threshold">-65</integer>
diff --git a/core/tests/coretests/src/android/content/pm/AppsQueryHelperTests.java b/core/tests/coretests/src/android/content/pm/AppsQueryHelperTests.java
index 4c9b00a..dcf2c89 100644
--- a/core/tests/coretests/src/android/content/pm/AppsQueryHelperTests.java
+++ b/core/tests/coretests/src/android/content/pm/AppsQueryHelperTests.java
@@ -33,7 +33,7 @@
@Override
public void setUp() throws Exception {
super.setUp();
- mAppsQueryHelper = new AppsQueryHelperTestable(getContext());
+ mAppsQueryHelper = new AppsQueryHelperTestable();
}
public void testQueryAppsSystemAppsOnly() {
@@ -78,33 +78,20 @@
assertEqualsIgnoreOrder(Arrays.asList("sys_app1", "sys_app3"), apps);
}
- public void testQueryAppsDefaultIme() {
- // Test query default system IMEs
- List<String> apps = mAppsQueryHelper.queryApps(AppsQueryHelper.GET_DEFAULT_IMES,
+ public void testQueryAppsImes() {
+ // Test query system IMEs
+ List<String> apps = mAppsQueryHelper.queryApps(AppsQueryHelper.GET_IMES,
true, UserHandle.of(UserHandle.myUserId()));
assertEqualsIgnoreOrder(Arrays.asList("sys_app1"), apps);
- // Test query default IMEs
- apps = mAppsQueryHelper.queryApps(AppsQueryHelper.GET_DEFAULT_IMES, false,
+ // Test query IMEs
+ apps = mAppsQueryHelper.queryApps(AppsQueryHelper.GET_IMES, false,
UserHandle.of(UserHandle.myUserId()));
assertEqualsIgnoreOrder(Arrays.asList("sys_app1", "app4"), apps);
-
- // Test that GET_DEFAULT_IMES cannot be used with a user id different from current process
- try {
- mAppsQueryHelper.queryApps(AppsQueryHelper.GET_DEFAULT_IMES, false,
- UserHandle.of(UserHandle.USER_NULL));
- fail("queryApps must fail if wrong user was passed");
- } catch (IllegalArgumentException e) {
- // OK
- }
}
private class AppsQueryHelperTestable extends AppsQueryHelper {
- public AppsQueryHelperTestable(Context context) {
- super(context);
- }
-
@Override
protected List<ApplicationInfo> getAllApps(int userId) {
final ApplicationInfo ai1 = new ApplicationInfo();
@@ -145,18 +132,19 @@
}
@Override
- protected List<InputMethodInfo> getInputMethodList() {
+ protected List<ResolveInfo> queryIntentServicesAsUser(Intent intent, int userId) {
final ResolveInfo sysApp1 = new ResolveInfo();
sysApp1.serviceInfo = new ServiceInfo();
sysApp1.serviceInfo.packageName = "sys_app1";
sysApp1.serviceInfo.name = "name";
- InputMethodInfo imi1 = new InputMethodInfo(sysApp1, false, null, null, 0, true);
+ sysApp1.serviceInfo.applicationInfo = new ApplicationInfo();
+ sysApp1.serviceInfo.applicationInfo.flags |= ApplicationInfo.FLAG_SYSTEM;
final ResolveInfo app4 = new ResolveInfo();
app4.serviceInfo = new ServiceInfo();
app4.serviceInfo.packageName = "app4";
app4.serviceInfo.name = "name";
- InputMethodInfo imi2 = new InputMethodInfo(app4, false, null, null, 0, true);
- return Arrays.asList(imi1, imi2);
+ app4.serviceInfo.applicationInfo = new ApplicationInfo();
+ return Arrays.asList(sysApp1, app4);
}
}
diff --git a/graphics/java/android/graphics/drawable/Drawable.java b/graphics/java/android/graphics/drawable/Drawable.java
index 39d13df..64f2698 100644
--- a/graphics/java/android/graphics/drawable/Drawable.java
+++ b/graphics/java/android/graphics/drawable/Drawable.java
@@ -913,16 +913,26 @@
protected void onBoundsChange(Rect bounds) {}
/**
- * Return the intrinsic width of the underlying drawable object. Returns
- * -1 if it has no intrinsic width, such as with a solid color.
+ * Returns the drawable's intrinsic width.
+ * <p>
+ * Intrinsic width is the width at which the drawable would like to be laid
+ * out, including any inherent padding. If the drawable has no intrinsic
+ * width, such as a solid color, this method returns -1.
+ *
+ * @return the intrinsic width, or -1 if no intrinsic width
*/
public int getIntrinsicWidth() {
return -1;
}
/**
- * Return the intrinsic height of the underlying drawable object. Returns
- * -1 if it has no intrinsic height, such as with a solid color.
+ * Returns the drawable's intrinsic height.
+ * <p>
+ * Intrinsic height is the height at which the drawable would like to be
+ * laid out, including any inherent padding. If the drawable has no
+ * intrinsic height, such as a solid color, this method returns -1.
+ *
+ * @return the intrinsic height, or -1 if no intrinsic height
*/
public int getIntrinsicHeight() {
return -1;
diff --git a/graphics/java/android/graphics/drawable/InsetDrawable.java b/graphics/java/android/graphics/drawable/InsetDrawable.java
index 927b9c9..36d4272 100644
--- a/graphics/java/android/graphics/drawable/InsetDrawable.java
+++ b/graphics/java/android/graphics/drawable/InsetDrawable.java
@@ -222,12 +222,20 @@
@Override
public int getIntrinsicWidth() {
- return getDrawable().getIntrinsicWidth() + mState.mInsetLeft + mState.mInsetRight;
+ final int childWidth = getDrawable().getIntrinsicWidth();
+ if (childWidth < 0) {
+ return -1;
+ }
+ return childWidth + mState.mInsetLeft + mState.mInsetRight;
}
@Override
public int getIntrinsicHeight() {
- return getDrawable().getIntrinsicHeight() + mState.mInsetTop + mState.mInsetBottom;
+ final int childHeight = getDrawable().getIntrinsicHeight();
+ if (childHeight < 0) {
+ return -1;
+ }
+ return childHeight + mState.mInsetTop + mState.mInsetBottom;
}
@Override
diff --git a/libs/hwui/Android.mk b/libs/hwui/Android.mk
index 5cdd723..4acad67 100644
--- a/libs/hwui/Android.mk
+++ b/libs/hwui/Android.mk
@@ -9,6 +9,7 @@
font/Font.cpp \
renderstate/Blend.cpp \
renderstate/MeshState.cpp \
+ renderstate/OffscreenBufferPool.cpp \
renderstate/PixelBufferState.cpp \
renderstate/RenderState.cpp \
renderstate/Scissor.cpp \
@@ -216,6 +217,7 @@
unit_tests/LayerUpdateQueueTests.cpp \
unit_tests/LinearAllocatorTests.cpp \
unit_tests/PathParserTests.cpp \
+ unit_tests/OffscreenBufferPoolTests.cpp \
unit_tests/StringUtilsTests.cpp
ifeq (true, $(HWUI_NEW_OPS))
diff --git a/libs/hwui/BakedOpRenderer.cpp b/libs/hwui/BakedOpRenderer.cpp
index 9380992..1aa291f 100644
--- a/libs/hwui/BakedOpRenderer.cpp
+++ b/libs/hwui/BakedOpRenderer.cpp
@@ -19,98 +19,22 @@
#include "Caches.h"
#include "Glop.h"
#include "GlopBuilder.h"
-#include "VertexBuffer.h"
+#include "renderstate/OffscreenBufferPool.h"
#include "renderstate/RenderState.h"
-#include "utils/FatVector.h"
#include "utils/GLUtils.h"
+#include "VertexBuffer.h"
namespace android {
namespace uirenderer {
////////////////////////////////////////////////////////////////////////////////
-// OffscreenBuffer
-////////////////////////////////////////////////////////////////////////////////
-
-OffscreenBuffer::OffscreenBuffer(RenderState& renderState, Caches& caches,
- uint32_t textureWidth, uint32_t textureHeight,
- uint32_t viewportWidth, uint32_t viewportHeight)
- : renderState(renderState)
- , viewportWidth(viewportWidth)
- , viewportHeight(viewportHeight)
- , texture(caches) {
- texture.width = textureWidth;
- texture.height = textureHeight;
-
- caches.textureState().activateTexture(0);
- glGenTextures(1, &texture.id);
- caches.textureState().bindTexture(GL_TEXTURE_2D, texture.id);
-
- texture.setWrap(GL_CLAMP_TO_EDGE, false, false, GL_TEXTURE_2D);
- // not setting filter on texture, since it's set when rendering, based on transform
-
- glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
- glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, texture.width, texture.height, 0,
- GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
-}
-
-void OffscreenBuffer::updateMeshFromRegion() {
- // avoid T-junctions as they cause artifacts in between the resultant
- // geometry when complex transforms occur.
- // TODO: generate the safeRegion only if necessary based on rendering transform
- Region safeRegion = Region::createTJunctionFreeRegion(region);
-
- size_t count;
- const android::Rect* rects = safeRegion.getArray(&count);
-
- const float texX = 1.0f / float(viewportWidth);
- const float texY = 1.0f / float(viewportHeight);
-
- FatVector<TextureVertex, 64> meshVector(count * 4); // uses heap if more than 64 vertices needed
- TextureVertex* mesh = &meshVector[0];
- for (size_t i = 0; i < count; i++) {
- const android::Rect* r = &rects[i];
-
- const float u1 = r->left * texX;
- const float v1 = (viewportHeight - r->top) * texY;
- const float u2 = r->right * texX;
- const float v2 = (viewportHeight - r->bottom) * texY;
-
- TextureVertex::set(mesh++, r->left, r->top, u1, v1);
- TextureVertex::set(mesh++, r->right, r->top, u2, v1);
- TextureVertex::set(mesh++, r->left, r->bottom, u1, v2);
- TextureVertex::set(mesh++, r->right, r->bottom, u2, v2);
- }
- elementCount = count * 6;
- renderState.meshState().genOrUpdateMeshBuffer(&vbo,
- sizeof(TextureVertex) * count * 4,
- &meshVector[0],
- GL_DYNAMIC_DRAW); // TODO: GL_STATIC_DRAW if savelayer
-}
-
-OffscreenBuffer::~OffscreenBuffer() {
- texture.deleteTexture();
- renderState.meshState().deleteMeshBuffer(vbo);
- elementCount = 0;
- vbo = 0;
-}
-
-////////////////////////////////////////////////////////////////////////////////
// BakedOpRenderer
////////////////////////////////////////////////////////////////////////////////
-OffscreenBuffer* BakedOpRenderer::createOffscreenBuffer(RenderState& renderState,
- uint32_t width, uint32_t height) {
- // TODO: get from cache!
- return new OffscreenBuffer(renderState, Caches::getInstance(), width, height, width, height);
-}
-
-void BakedOpRenderer::destroyOffscreenBuffer(OffscreenBuffer* offscreenBuffer) {
- // TODO: return texture/offscreenbuffer to cache!
- delete offscreenBuffer;
-}
-
OffscreenBuffer* BakedOpRenderer::startTemporaryLayer(uint32_t width, uint32_t height) {
- OffscreenBuffer* buffer = createOffscreenBuffer(mRenderState, width, height);
+ LOG_ALWAYS_FATAL_IF(mRenderTarget.offscreenBuffer, "already has layer...");
+
+ OffscreenBuffer* buffer = mRenderState.layerPool().get(mRenderState, width, height);
startRepaintLayer(buffer);
return buffer;
}
@@ -357,7 +281,7 @@
renderer.renderGlop(state, glop);
if (op.destroy) {
- BakedOpRenderer::destroyOffscreenBuffer(buffer);
+ renderer.renderState().layerPool().putOrDelete(buffer);
}
}
diff --git a/libs/hwui/BakedOpRenderer.h b/libs/hwui/BakedOpRenderer.h
index 4c3a2d0..d6d9cb1 100644
--- a/libs/hwui/BakedOpRenderer.h
+++ b/libs/hwui/BakedOpRenderer.h
@@ -29,33 +29,6 @@
class RenderState;
/**
- * Lightweight alternative to Layer. Owns the persistent state of an offscreen render target, and
- * encompasses enough information to draw it back on screen (minus paint properties, which are held
- * by LayerOp).
- */
-class OffscreenBuffer {
-public:
- OffscreenBuffer(RenderState& renderState, Caches& caches,
- uint32_t textureWidth, uint32_t textureHeight,
- uint32_t viewportWidth, uint32_t viewportHeight);
- ~OffscreenBuffer();
-
- // must be called prior to rendering, to construct/update vertex buffer
- void updateMeshFromRegion();
-
- RenderState& renderState;
- uint32_t viewportWidth;
- uint32_t viewportHeight;
- Texture texture;
-
- // Portion of offscreen buffer that has been drawn to. Used to minimize drawing area when
- // drawing back to screen / parent FBO.
- Region region;
- GLsizei elementCount = 0;
- GLuint vbo = 0;
-};
-
-/**
* Main rendering manager for a collection of work - one frame + any contained FBOs.
*
* Manages frame and FBO lifecycle, binding the GL framebuffer as appropriate. This is the only
@@ -72,10 +45,6 @@
, mOpaque(opaque) {
}
- static OffscreenBuffer* createOffscreenBuffer(RenderState& renderState,
- uint32_t width, uint32_t height);
- static void destroyOffscreenBuffer(OffscreenBuffer*);
-
RenderState& renderState() { return mRenderState; }
Caches& caches() { return mCaches; }
diff --git a/libs/hwui/DisplayListCanvas.h b/libs/hwui/DisplayListCanvas.h
index fc08504..609103b 100644
--- a/libs/hwui/DisplayListCanvas.h
+++ b/libs/hwui/DisplayListCanvas.h
@@ -17,6 +17,14 @@
#ifndef ANDROID_HWUI_DISPLAY_LIST_RENDERER_H
#define ANDROID_HWUI_DISPLAY_LIST_RENDERER_H
+#include "Canvas.h"
+#include "CanvasState.h"
+#include "DisplayList.h"
+#include "RenderNode.h"
+#include "ResourceCache.h"
+#include "SkiaCanvasProxy.h"
+#include "utils/Macros.h"
+
#include <SkDrawFilter.h>
#include <SkMatrix.h>
#include <SkPaint.h>
@@ -25,13 +33,6 @@
#include <SkTLazy.h>
#include <cutils/compiler.h>
-#include "Canvas.h"
-#include "CanvasState.h"
-#include "DisplayList.h"
-#include "SkiaCanvasProxy.h"
-#include "RenderNode.h"
-#include "ResourceCache.h"
-
namespace android {
namespace uirenderer {
@@ -66,7 +67,7 @@
virtual ~DisplayListCanvas();
void reset(int width, int height);
- __attribute__((warn_unused_result)) DisplayList* finishRecording();
+ WARN_UNUSED_RESULT DisplayList* finishRecording();
// ----------------------------------------------------------------------------
// HWUI Canvas state operations
diff --git a/libs/hwui/LayerCache.cpp b/libs/hwui/LayerCache.cpp
index 39cadd1..b117754 100644
--- a/libs/hwui/LayerCache.cpp
+++ b/libs/hwui/LayerCache.cpp
@@ -14,13 +14,14 @@
* limitations under the License.
*/
-#include <GLES2/gl2.h>
+#include "LayerCache.h"
+
+#include "Caches.h"
+#include "Properties.h"
#include <utils/Log.h>
-#include "Caches.h"
-#include "LayerCache.h"
-#include "Properties.h"
+#include <GLES2/gl2.h>
namespace android {
namespace uirenderer {
@@ -29,15 +30,9 @@
// Constructors/destructor
///////////////////////////////////////////////////////////////////////////////
-LayerCache::LayerCache(): mSize(0), mMaxSize(MB(DEFAULT_LAYER_CACHE_SIZE)) {
- char property[PROPERTY_VALUE_MAX];
- if (property_get(PROPERTY_LAYER_CACHE_SIZE, property, nullptr) > 0) {
- INIT_LOGD(" Setting layer cache size to %sMB", property);
- setMaxSize(MB(atof(property)));
- } else {
- INIT_LOGD(" Using default layer cache size of %.2fMB", DEFAULT_LAYER_CACHE_SIZE);
- }
-}
+LayerCache::LayerCache()
+ : mSize(0)
+ , mMaxSize(Properties::layerPoolSize) {}
LayerCache::~LayerCache() {
clear();
diff --git a/libs/hwui/PathParser.cpp b/libs/hwui/PathParser.cpp
index 61b9d21..35230ff 100644
--- a/libs/hwui/PathParser.cpp
+++ b/libs/hwui/PathParser.cpp
@@ -18,6 +18,7 @@
#include "jni.h"
+#include <errno.h>
#include <utils/Log.h>
#include <sstream>
#include <stdlib.h>
@@ -97,14 +98,31 @@
*outEndPosition = currentIndex;
}
+static float parseFloat(PathParser::ParseResult* result, const char* startPtr, size_t expectedLength) {
+ char* endPtr = NULL;
+ float currentValue = strtof(startPtr, &endPtr);
+ if ((currentValue == HUGE_VALF || currentValue == -HUGE_VALF) && errno == ERANGE) {
+ result->failureOccurred = true;
+ result->failureMessage = "Float out of range: ";
+ result->failureMessage.append(startPtr, expectedLength);
+ }
+ if (currentValue == 0 && endPtr == startPtr) {
+ // No conversion is done.
+ result->failureOccurred = true;
+ result->failureMessage = "Float format error when parsing: ";
+ result->failureMessage.append(startPtr, expectedLength);
+ }
+ return currentValue;
+}
+
/**
-* Parse the floats in the string.
-* This is an optimized version of parseFloat(s.split(",|\\s"));
-*
-* @param s the string containing a command and list of floats
-* @return array of floats
-*/
-static void getFloats(std::vector<float>* outPoints, const char* pathStr, int start, int end) {
+ * Parse the floats in the string.
+ *
+ * @param s the string containing a command and list of floats
+ * @return true on success
+ */
+static void getFloats(std::vector<float>* outPoints, PathParser::ParseResult* result,
+ const char* pathStr, int start, int end) {
if (pathStr[start] == 'z' || pathStr[start] == 'Z') {
return;
@@ -120,7 +138,12 @@
extract(&endPosition, &endWithNegOrDot, pathStr, startPosition, end);
if (startPosition < endPosition) {
- outPoints->push_back(strtof(&pathStr[startPosition], NULL));
+ float currentValue = parseFloat(result, &pathStr[startPosition],
+ end - startPosition);
+ if (result->failureOccurred) {
+ return;
+ }
+ outPoints->push_back(currentValue);
}
if (endWithNegOrDot) {
@@ -130,10 +153,14 @@
startPosition = endPosition + 1;
}
}
+ return;
}
-void PathParser::getPathDataFromString(PathData* data, const char* pathStr, size_t strLen) {
+void PathParser::getPathDataFromString(PathData* data, ParseResult* result,
+ const char* pathStr, size_t strLen) {
if (pathStr == NULL) {
+ result->failureOccurred = true;
+ result->failureMessage = "Path string cannot be NULL.";
return;
}
@@ -143,7 +170,10 @@
while (end < strLen) {
end = nextStart(pathStr, strLen, end);
std::vector<float> points;
- getFloats(&points, pathStr, start, end);
+ getFloats(&points, result, pathStr, start, end);
+ if (result->failureOccurred) {
+ return;
+ }
data->verbs.push_back(pathStr[start]);
data->verbSizes.push_back(points.size());
data->points.insert(data->points.end(), points.begin(), points.end());
@@ -151,16 +181,11 @@
end++;
}
- if ((end - start) == 1 && pathStr[start] != '\0') {
+ if ((end - start) == 1 && start < strLen) {
data->verbs.push_back(pathStr[start]);
data->verbSizes.push_back(0);
}
-
- int i = 0;
- while(pathStr[i] != '\0') {
- i++;
- }
-
+ return;
}
void PathParser::dump(const PathData& data) {
@@ -169,6 +194,7 @@
for (size_t i = 0; i < data.verbs.size(); i++) {
std::ostringstream os;
os << data.verbs[i];
+ os << ", verb size: " << data.verbSizes[i];
for (size_t j = 0; j < data.verbSizes[i]; j++) {
os << " " << data.points[start + j];
}
@@ -183,16 +209,20 @@
ALOGD("points are : %s", os.str().c_str());
}
-bool PathParser::parseStringForSkPath(SkPath* skPath, const char* pathStr, size_t strLen) {
+void PathParser::parseStringForSkPath(SkPath* skPath, ParseResult* result, const char* pathStr, size_t strLen) {
PathData pathData;
- getPathDataFromString(&pathData, pathStr, strLen);
-
+ getPathDataFromString(&pathData, result, pathStr, strLen);
+ if (result->failureOccurred) {
+ return;
+ }
// Check if there is valid data coming out of parsing the string.
if (pathData.verbs.size() == 0) {
- return false;
+ result->failureOccurred = true;
+ result->failureMessage = "No verbs found in the string for pathData";
+ return;
}
VectorDrawablePath::verbsToPath(skPath, &pathData);
- return true;
+ return;
}
}; // namespace uirenderer
diff --git a/libs/hwui/PathParser.h b/libs/hwui/PathParser.h
index d30bb0f..a9c1e60 100644
--- a/libs/hwui/PathParser.h
+++ b/libs/hwui/PathParser.h
@@ -23,17 +23,25 @@
#include <android/log.h>
#include <cutils/compiler.h>
+#include <string>
+
namespace android {
namespace uirenderer {
+
class PathParser {
public:
+ struct ANDROID_API ParseResult {
+ bool failureOccurred = false;
+ std::string failureMessage;
+ };
/**
* Parse the string literal and create a Skia Path. Return true on success.
*/
- ANDROID_API static bool parseStringForSkPath(SkPath* outPath, const char* pathStr,
- size_t strLength);
- static void getPathDataFromString(PathData* outData, const char* pathStr, size_t strLength);
+ ANDROID_API static void parseStringForSkPath(SkPath* outPath, ParseResult* result,
+ const char* pathStr, size_t strLength);
+ static void getPathDataFromString(PathData* outData, ParseResult* result,
+ const char* pathStr, size_t strLength);
static void dump(const PathData& data);
};
diff --git a/libs/hwui/Properties.cpp b/libs/hwui/Properties.cpp
index e818186..0669596 100644
--- a/libs/hwui/Properties.cpp
+++ b/libs/hwui/Properties.cpp
@@ -37,6 +37,7 @@
bool Properties::enablePartialUpdates = true;
float Properties::textGamma = DEFAULT_TEXT_GAMMA;
+int Properties::layerPoolSize = DEFAULT_LAYER_CACHE_SIZE;
DebugLevel Properties::debugLevel = kDebugDisabled;
OverdrawColorSet Properties::overdrawColorSet = OverdrawColorSet::Default;
@@ -52,10 +53,19 @@
ProfileType Properties::sProfileType = ProfileType::None;
bool Properties::sDisableProfileBars = false;
+static int property_get_int(const char* key, int defaultValue) {
+ char buf[PROPERTY_VALUE_MAX] = {'\0',};
+
+ if (property_get(key, buf, "") > 0) {
+ return atoi(buf);
+ }
+ return defaultValue;
+}
+
static float property_get_float(const char* key, float defaultValue) {
char buf[PROPERTY_VALUE_MAX] = {'\0',};
- if (property_get(PROPERTY_PROFILE, buf, "") > 0) {
+ if (property_get(key, buf, "") > 0) {
return atof(buf);
}
return defaultValue;
@@ -114,16 +124,14 @@
showDirtyRegions = property_get_bool(PROPERTY_DEBUG_SHOW_DIRTY_REGIONS, false);
- debugLevel = kDebugDisabled;
- if (property_get(PROPERTY_DEBUG, property, nullptr) > 0) {
- debugLevel = (DebugLevel) atoi(property);
- }
+ debugLevel = (DebugLevel) property_get_int(PROPERTY_DEBUG, kDebugDisabled);
skipEmptyFrames = property_get_bool(PROPERTY_SKIP_EMPTY_DAMAGE, true);
useBufferAge = property_get_bool(PROPERTY_USE_BUFFER_AGE, true);
enablePartialUpdates = property_get_bool(PROPERTY_ENABLE_PARTIAL_UPDATES, true);
textGamma = property_get_float(PROPERTY_TEXT_GAMMA, DEFAULT_TEXT_GAMMA);
+ layerPoolSize = MB(property_get_float(PROPERTY_LAYER_CACHE_SIZE, DEFAULT_LAYER_CACHE_SIZE));
return (prevDebugLayersUpdates != debugLayersUpdates)
|| (prevDebugOverdraw != debugOverdraw)
diff --git a/libs/hwui/Properties.h b/libs/hwui/Properties.h
index 1293c78..1dde7e0 100644
--- a/libs/hwui/Properties.h
+++ b/libs/hwui/Properties.h
@@ -267,6 +267,8 @@
static float textGamma;
+ static int layerPoolSize;
+
static DebugLevel debugLevel;
static OverdrawColorSet overdrawColorSet;
static StencilClipDebug debugStencilClip;
diff --git a/libs/hwui/RecordingCanvas.h b/libs/hwui/RecordingCanvas.h
index fc84c98..f26b0c8 100644
--- a/libs/hwui/RecordingCanvas.h
+++ b/libs/hwui/RecordingCanvas.h
@@ -20,11 +20,12 @@
#include "Canvas.h"
#include "CanvasState.h"
#include "DisplayList.h"
-#include "utils/LinearAllocator.h"
-#include "utils/NinePatch.h"
#include "ResourceCache.h"
#include "SkiaCanvasProxy.h"
#include "Snapshot.h"
+#include "utils/LinearAllocator.h"
+#include "utils/Macros.h"
+#include "utils/NinePatch.h"
#include <SkDrawFilter.h>
#include <SkPaint.h>
@@ -49,7 +50,7 @@
virtual ~RecordingCanvas();
void reset(int width, int height);
- __attribute__((warn_unused_result)) DisplayList* finishRecording();
+ WARN_UNUSED_RESULT DisplayList* finishRecording();
// ----------------------------------------------------------------------------
// MISC HWUI OPERATIONS - TODO: CATEGORIZE
diff --git a/libs/hwui/RenderNode.cpp b/libs/hwui/RenderNode.cpp
index 15ca718..e177f9a 100644
--- a/libs/hwui/RenderNode.cpp
+++ b/libs/hwui/RenderNode.cpp
@@ -248,22 +248,31 @@
}
}
-layer_t* createLayer(RenderState& renderState, uint32_t width, uint32_t height) {
+static layer_t* createLayer(RenderState& renderState, uint32_t width, uint32_t height) {
#if HWUI_NEW_OPS
- return BakedOpRenderer::createOffscreenBuffer(renderState, width, height);
+ return renderState.layerPool().get(renderState, width, height);
#else
return LayerRenderer::createRenderLayer(renderState, width, height);
#endif
}
-void destroyLayer(layer_t* layer) {
+static void destroyLayer(layer_t* layer) {
#if HWUI_NEW_OPS
- BakedOpRenderer::destroyOffscreenBuffer(layer);
+ RenderState& renderState = layer->renderState;
+ renderState.layerPool().putOrDelete(layer);
#else
LayerRenderer::destroyLayer(layer);
#endif
}
+static bool layerMatchesWidthAndHeight(layer_t* layer, int width, int height) {
+#if HWUI_NEW_OPS
+ return layer->viewportWidth == (uint32_t) width && layer->viewportHeight == (uint32_t)height;
+#else
+ return layer->layer.getWidth() == width && layer->layer.getHeight() == height;
+#endif
+}
+
void RenderNode::pushLayerUpdate(TreeInfo& info) {
LayerType layerType = properties().effectiveLayerType();
// If we are not a layer OR we cannot be rendered (eg, view was detached)
@@ -278,17 +287,16 @@
bool transformUpdateNeeded = false;
if (!mLayer) {
- mLayer = createLayer(info.canvasContext.getRenderState(), getWidth(), getHeight());
- damageSelf(info);
- transformUpdateNeeded = true;
+ mLayer = createLayer(info.canvasContext.getRenderState(), getWidth(), getHeight());
+ damageSelf(info);
+ transformUpdateNeeded = true;
+ } else if (!layerMatchesWidthAndHeight(mLayer, getWidth(), getHeight())) {
#if HWUI_NEW_OPS
- } else if (mLayer->viewportWidth != (uint32_t) getWidth()
- || mLayer->viewportHeight != (uint32_t)getHeight()) {
- // TODO: allow node's layer to grow larger
- if ((uint32_t)getWidth() > mLayer->texture.width
- || (uint32_t)getHeight() > mLayer->texture.height) {
+ RenderState& renderState = mLayer->renderState;
+ if (properties().fitsOnLayer()) {
+ mLayer = renderState.layerPool().resize(mLayer, getWidth(), getHeight());
+ } else {
#else
- } else if (mLayer->layer.getWidth() != getWidth() || mLayer->layer.getHeight() != getHeight()) {
if (!LayerRenderer::resizeLayer(mLayer, getWidth(), getHeight())) {
#endif
destroyLayer(mLayer);
diff --git a/libs/hwui/RenderProperties.h b/libs/hwui/RenderProperties.h
index ca7789e..0bd5b65 100644
--- a/libs/hwui/RenderProperties.h
+++ b/libs/hwui/RenderProperties.h
@@ -608,12 +608,16 @@
&& getOutline().getAlpha() != 0.0f;
}
- bool promotedToLayer() const {
+ bool fitsOnLayer() const {
const DeviceInfo* deviceInfo = DeviceInfo::get();
LOG_ALWAYS_FATAL_IF(!deviceInfo, "DeviceInfo uninitialized");
+ return mPrimitiveFields.mWidth <= deviceInfo->maxTextureSize()
+ && mPrimitiveFields.mHeight <= deviceInfo->maxTextureSize();
+ }
+
+ bool promotedToLayer() const {
return mLayerProperties.mType == LayerType::None
- && mPrimitiveFields.mWidth <= deviceInfo->maxTextureSize()
- && mPrimitiveFields.mHeight <= deviceInfo->maxTextureSize()
+ && fitsOnLayer()
&& (mComputedFields.mNeedLayerForFunctors
|| (!MathUtils::isZero(mPrimitiveFields.mAlpha)
&& mPrimitiveFields.mAlpha < 1
diff --git a/libs/hwui/VectorDrawablePath.cpp b/libs/hwui/VectorDrawablePath.cpp
index 115435c..05ea2da 100644
--- a/libs/hwui/VectorDrawablePath.cpp
+++ b/libs/hwui/VectorDrawablePath.cpp
@@ -37,8 +37,11 @@
};
VectorDrawablePath::VectorDrawablePath(const char* pathStr, size_t strLength) {
- PathParser::getPathDataFromString(&mData, pathStr, strLength);
- verbsToPath(&mSkPath, &mData);
+ PathParser::ParseResult result;
+ PathParser::getPathDataFromString(&mData, &result, pathStr, strLength);
+ if (!result.failureOccurred) {
+ verbsToPath(&mSkPath, &mData);
+ }
}
VectorDrawablePath::VectorDrawablePath(const PathData& data) {
@@ -80,7 +83,7 @@
for (unsigned int i = 0; i < data->verbs.size(); i++) {
size_t verbSize = data->verbSizes[i];
resolver.addCommand(outPath, previousCommand, data->verbs[i], &data->points, start,
- start + verbSize - 1u);
+ start + verbSize);
previousCommand = data->verbs[i];
start += verbSize;
}
@@ -254,7 +257,7 @@
arcToBezier(p, cx, cy, a, b, x0, y0, thetaD, eta0, sweep);
}
-
+// Use the given verb, and points in the range [start, end) to insert a command into the SkPath.
void PathResolver::addCommand(SkPath* outPath, char previousCmd,
char cmd, const std::vector<float>* points, size_t start, size_t end) {
@@ -305,7 +308,7 @@
break;
}
- for (unsigned int k = start; k <= end; k += incr) {
+ for (unsigned int k = start; k < end; k += incr) {
switch (cmd) {
case 'm': // moveto - Start a new sub-path (relative)
currentX += points->at(k + 0);
diff --git a/libs/hwui/microbench/PathParserBench.cpp b/libs/hwui/microbench/PathParserBench.cpp
index 198035e..171078d 100644
--- a/libs/hwui/microbench/PathParserBench.cpp
+++ b/libs/hwui/microbench/PathParserBench.cpp
@@ -28,9 +28,10 @@
const char* pathString = "M 1 1 m 2 2, l 3 3 L 3 3 H 4 h4 V5 v5, Q6 6 6 6 q 6 6 6 6t 7 7 T 7 7 C 8 8 8 8 8 8 c 8 8 8 8 8 8 S 9 9 9 9 s 9 9 9 9 A 10 10 0 1 1 10 10 a 10 10 0 1 1 10 10";
SkPath skPath;
size_t length = strlen(pathString);
+ PathParser::ParseResult result;
StartBenchmarkTiming();
for (int i = 0; i < iter; i++) {
- PathParser::parseStringForSkPath(&skPath, pathString, length);
+ PathParser::parseStringForSkPath(&skPath, &result, pathString, length);
}
StopBenchmarkTiming();
}
diff --git a/libs/hwui/renderstate/OffscreenBufferPool.cpp b/libs/hwui/renderstate/OffscreenBufferPool.cpp
new file mode 100644
index 0000000..6b44557
--- /dev/null
+++ b/libs/hwui/renderstate/OffscreenBufferPool.cpp
@@ -0,0 +1,196 @@
+/*
+ * Copyright (C) 2015 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.
+ */
+
+#include "OffscreenBufferPool.h"
+
+#include "Caches.h"
+#include "Properties.h"
+#include "renderstate/RenderState.h"
+#include "utils/FatVector.h"
+
+#include <utils/Log.h>
+
+#include <GLES2/gl2.h>
+
+namespace android {
+namespace uirenderer {
+
+////////////////////////////////////////////////////////////////////////////////
+// OffscreenBuffer
+////////////////////////////////////////////////////////////////////////////////
+
+OffscreenBuffer::OffscreenBuffer(RenderState& renderState, Caches& caches,
+ uint32_t viewportWidth, uint32_t viewportHeight)
+ : renderState(renderState)
+ , viewportWidth(viewportWidth)
+ , viewportHeight(viewportHeight)
+ , texture(caches) {
+ texture.width = computeIdealDimension(viewportWidth);
+ texture.height = computeIdealDimension(viewportHeight);
+ texture.blend = true;
+
+ caches.textureState().activateTexture(0);
+ glGenTextures(1, &texture.id);
+ caches.textureState().bindTexture(GL_TEXTURE_2D, texture.id);
+
+ texture.setWrap(GL_CLAMP_TO_EDGE, false, false, GL_TEXTURE_2D);
+ // not setting filter on texture, since it's set when drawing, based on transform
+
+ glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, texture.width, texture.height, 0,
+ GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+}
+
+void OffscreenBuffer::updateMeshFromRegion() {
+ // avoid T-junctions as they cause artifacts in between the resultant
+ // geometry when complex transforms occur.
+ // TODO: generate the safeRegion only if necessary based on drawing transform
+ Region safeRegion = Region::createTJunctionFreeRegion(region);
+
+ size_t count;
+ const android::Rect* rects = safeRegion.getArray(&count);
+
+ const float texX = 1.0f / float(texture.width);
+ const float texY = 1.0f / float(texture.height);
+
+ FatVector<TextureVertex, 64> meshVector(count * 4); // uses heap if more than 64 vertices needed
+ TextureVertex* mesh = &meshVector[0];
+ for (size_t i = 0; i < count; i++) {
+ const android::Rect* r = &rects[i];
+
+ const float u1 = r->left * texX;
+ const float v1 = (viewportHeight - r->top) * texY;
+ const float u2 = r->right * texX;
+ const float v2 = (viewportHeight - r->bottom) * texY;
+
+ TextureVertex::set(mesh++, r->left, r->top, u1, v1);
+ TextureVertex::set(mesh++, r->right, r->top, u2, v1);
+ TextureVertex::set(mesh++, r->left, r->bottom, u1, v2);
+ TextureVertex::set(mesh++, r->right, r->bottom, u2, v2);
+ }
+ elementCount = count * 6;
+ renderState.meshState().genOrUpdateMeshBuffer(&vbo,
+ sizeof(TextureVertex) * count * 4,
+ &meshVector[0],
+ GL_DYNAMIC_DRAW); // TODO: GL_STATIC_DRAW if savelayer
+}
+
+uint32_t OffscreenBuffer::computeIdealDimension(uint32_t dimension) {
+ return uint32_t(ceilf(dimension / float(LAYER_SIZE)) * LAYER_SIZE);
+}
+
+OffscreenBuffer::~OffscreenBuffer() {
+ texture.deleteTexture();
+ renderState.meshState().deleteMeshBuffer(vbo);
+ elementCount = 0;
+ vbo = 0;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// OffscreenBufferPool
+///////////////////////////////////////////////////////////////////////////////
+
+OffscreenBufferPool::OffscreenBufferPool()
+ : mMaxSize(Properties::layerPoolSize) {
+}
+
+OffscreenBufferPool::~OffscreenBufferPool() {
+ clear(); // TODO: unique_ptr?
+}
+
+int OffscreenBufferPool::Entry::compare(const Entry& lhs, const Entry& rhs) {
+ int deltaInt = int(lhs.width) - int(rhs.width);
+ if (deltaInt != 0) return deltaInt;
+
+ return int(lhs.height) - int(rhs.height);
+}
+
+void OffscreenBufferPool::clear() {
+ for (auto entry : mPool) {
+ delete entry.layer;
+ }
+ mPool.clear();
+ mSize = 0;
+}
+
+OffscreenBuffer* OffscreenBufferPool::get(RenderState& renderState,
+ const uint32_t width, const uint32_t height) {
+ OffscreenBuffer* layer = nullptr;
+
+ Entry entry(width, height);
+ auto iter = mPool.find(entry);
+
+ if (iter != mPool.end()) {
+ entry = *iter;
+ mPool.erase(iter);
+
+ layer = entry.layer;
+ layer->viewportWidth = width;
+ layer->viewportHeight = height;
+ mSize -= layer->getSizeInBytes();
+ } else {
+ layer = new OffscreenBuffer(renderState, Caches::getInstance(), width, height);
+ }
+
+ return layer;
+}
+
+OffscreenBuffer* OffscreenBufferPool::resize(OffscreenBuffer* layer,
+ const uint32_t width, const uint32_t height) {
+ RenderState& renderState = layer->renderState;
+ if (layer->texture.width == OffscreenBuffer::computeIdealDimension(width)
+ && layer->texture.height == OffscreenBuffer::computeIdealDimension(height)) {
+ // resize in place
+ layer->viewportWidth = width;
+ layer->viewportHeight = height;
+ return layer;
+ }
+ putOrDelete(layer);
+ return get(renderState, width, height);
+}
+
+void OffscreenBufferPool::dump() {
+ for (auto entry : mPool) {
+ ALOGD(" Layer size %dx%d", entry.width, entry.height);
+ }
+}
+
+void OffscreenBufferPool::putOrDelete(OffscreenBuffer* layer) {
+ const uint32_t size = layer->getSizeInBytes();
+ // Don't even try to cache a layer that's bigger than the cache
+ if (size < mMaxSize) {
+ // TODO: Use an LRU
+ while (mSize + size > mMaxSize) {
+ OffscreenBuffer* victim = mPool.begin()->layer;
+ mSize -= victim->getSizeInBytes();
+ delete victim;
+ mPool.erase(mPool.begin());
+ }
+
+ // clear region, since it's no longer valid
+ layer->region.clear();
+
+ Entry entry(layer);
+
+ mPool.insert(entry);
+ mSize += size;
+ } else {
+ delete layer;
+ }
+}
+
+}; // namespace uirenderer
+}; // namespace android
diff --git a/libs/hwui/renderstate/OffscreenBufferPool.h b/libs/hwui/renderstate/OffscreenBufferPool.h
new file mode 100644
index 0000000..f0fd82d
--- /dev/null
+++ b/libs/hwui/renderstate/OffscreenBufferPool.h
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2015 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.
+ */
+
+#ifndef ANDROID_HWUI_OFFSCREEN_BUFFER_POOL_H
+#define ANDROID_HWUI_OFFSCREEN_BUFFER_POOL_H
+
+#include "Caches.h"
+#include "Texture.h"
+#include "utils/Macros.h"
+
+#include <ui/Region.h>
+
+#include <set>
+
+namespace android {
+namespace uirenderer {
+
+class RenderState;
+
+/**
+ * Lightweight alternative to Layer. Owns the persistent state of an offscreen render target, and
+ * encompasses enough information to draw it back on screen (minus paint properties, which are held
+ * by LayerOp).
+ */
+class OffscreenBuffer {
+public:
+ OffscreenBuffer(RenderState& renderState, Caches& caches,
+ uint32_t viewportWidth, uint32_t viewportHeight);
+ ~OffscreenBuffer();
+
+ // must be called prior to rendering, to construct/update vertex buffer
+ void updateMeshFromRegion();
+
+ static uint32_t computeIdealDimension(uint32_t dimension);
+
+ uint32_t getSizeInBytes() { return texture.width * texture.height * 4; }
+
+ RenderState& renderState;
+ uint32_t viewportWidth;
+ uint32_t viewportHeight;
+ Texture texture;
+
+ // Portion of layer that has been drawn to. Used to minimize drawing area when
+ // drawing back to screen / parent FBO.
+ Region region;
+ GLsizei elementCount = 0;
+ GLuint vbo = 0;
+};
+
+/**
+ * Pool of OffscreenBuffers allocated, but not currently in use.
+ */
+class OffscreenBufferPool {
+public:
+ OffscreenBufferPool();
+ ~OffscreenBufferPool();
+
+ WARN_UNUSED_RESULT OffscreenBuffer* get(RenderState& renderState,
+ const uint32_t width, const uint32_t height);
+
+ WARN_UNUSED_RESULT OffscreenBuffer* resize(OffscreenBuffer* layer,
+ const uint32_t width, const uint32_t height);
+
+ void putOrDelete(OffscreenBuffer* layer);
+
+ /**
+ * Clears the pool. This causes all layers to be deleted.
+ */
+ void clear();
+
+ /**
+ * Returns the maximum size of the pool in bytes.
+ */
+ uint32_t getMaxSize() { return mMaxSize; }
+
+ /**
+ * Returns the current size of the pool in bytes.
+ */
+ uint32_t getSize() { return mSize; }
+
+ size_t getCount() { return mPool.size(); }
+
+ /**
+ * Prints out the content of the pool.
+ */
+ void dump();
+private:
+ struct Entry {
+ Entry() {}
+
+ Entry(const uint32_t layerWidth, const uint32_t layerHeight)
+ : width(OffscreenBuffer::computeIdealDimension(layerWidth))
+ , height(OffscreenBuffer::computeIdealDimension(layerHeight)) {}
+
+ Entry(OffscreenBuffer* layer)
+ : layer(layer)
+ , width(layer->texture.width)
+ , height(layer->texture.height) {
+ }
+
+ static int compare(const Entry& lhs, const Entry& rhs);
+
+ bool operator==(const Entry& other) const {
+ return compare(*this, other) == 0;
+ }
+
+ bool operator!=(const Entry& other) const {
+ return compare(*this, other) != 0;
+ }
+
+ bool operator<(const Entry& other) const {
+ return Entry::compare(*this, other) < 0;
+ }
+
+ OffscreenBuffer* layer = nullptr;
+ uint32_t width = 0;
+ uint32_t height = 0;
+ }; // struct Entry
+
+ std::multiset<Entry> mPool;
+
+ uint32_t mSize = 0;
+ uint32_t mMaxSize;
+}; // class OffscreenBufferCache
+
+}; // namespace uirenderer
+}; // namespace android
+
+#endif // ANDROID_HWUI_OFFSCREEN_BUFFER_POOL_H
diff --git a/libs/hwui/renderstate/RenderState.cpp b/libs/hwui/renderstate/RenderState.cpp
index 9637117..4fa8200 100644
--- a/libs/hwui/renderstate/RenderState.cpp
+++ b/libs/hwui/renderstate/RenderState.cpp
@@ -90,6 +90,8 @@
}
*/
+ mLayerPool.clear();
+
// TODO: reset all cached state in state objects
std::for_each(mActiveLayers.begin(), mActiveLayers.end(), layerLostGlContext);
mAssetAtlas.terminate();
@@ -106,6 +108,19 @@
mStencil = nullptr;
}
+void RenderState::flush(Caches::FlushMode mode) {
+ switch (mode) {
+ case Caches::FlushMode::Full:
+ // fall through
+ case Caches::FlushMode::Moderate:
+ // fall through
+ case Caches::FlushMode::Layers:
+ mLayerPool.clear();
+ break;
+ }
+ mCaches->flush(mode);
+}
+
void RenderState::setViewport(GLsizei width, GLsizei height) {
mViewportWidth = width;
mViewportHeight = height;
diff --git a/libs/hwui/renderstate/RenderState.h b/libs/hwui/renderstate/RenderState.h
index 3cda170..dcd5ea6 100644
--- a/libs/hwui/renderstate/RenderState.h
+++ b/libs/hwui/renderstate/RenderState.h
@@ -21,6 +21,7 @@
#include "Glop.h"
#include "renderstate/Blend.h"
#include "renderstate/MeshState.h"
+#include "renderstate/OffscreenBufferPool.h"
#include "renderstate/PixelBufferState.h"
#include "renderstate/Scissor.h"
#include "renderstate/Stencil.h"
@@ -56,6 +57,8 @@
void onGLContextCreated();
void onGLContextDestroyed();
+ void flush(Caches::FlushMode flushMode);
+
void setViewport(GLsizei width, GLsizei height);
void getViewport(GLsizei* outWidth, GLsizei* outHeight);
@@ -97,6 +100,8 @@
Scissor& scissor() { return *mScissor; }
Stencil& stencil() { return *mStencil; }
+ OffscreenBufferPool& layerPool() { return mLayerPool; }
+
void dump();
private:
@@ -116,6 +121,8 @@
Scissor* mScissor = nullptr;
Stencil* mStencil = nullptr;
+ OffscreenBufferPool mLayerPool;
+
AssetAtlas mAssetAtlas;
std::set<Layer*> mActiveLayers;
std::set<renderthread::CanvasContext*> mRegisteredContexts;
diff --git a/libs/hwui/renderthread/CanvasContext.cpp b/libs/hwui/renderthread/CanvasContext.cpp
index 1b89960..f094b2d 100644
--- a/libs/hwui/renderthread/CanvasContext.cpp
+++ b/libs/hwui/renderthread/CanvasContext.cpp
@@ -599,7 +599,7 @@
// Make sure to release all the textures we were owning as there won't
// be another draw
caches.textureCache.resetMarkInUse(this);
- caches.flush(Caches::FlushMode::Layers);
+ mRenderThread.renderState().flush(Caches::FlushMode::Layers);
}
}
@@ -609,10 +609,10 @@
ATRACE_CALL();
if (level >= TRIM_MEMORY_COMPLETE) {
- Caches::getInstance().flush(Caches::FlushMode::Full);
+ thread.renderState().flush(Caches::FlushMode::Full);
thread.eglManager().destroy();
} else if (level >= TRIM_MEMORY_UI_HIDDEN) {
- Caches::getInstance().flush(Caches::FlushMode::Moderate);
+ thread.renderState().flush(Caches::FlushMode::Moderate);
}
}
diff --git a/libs/hwui/unit_tests/OffscreenBufferPoolTests.cpp b/libs/hwui/unit_tests/OffscreenBufferPoolTests.cpp
new file mode 100644
index 0000000..ba92157
--- /dev/null
+++ b/libs/hwui/unit_tests/OffscreenBufferPoolTests.cpp
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2015 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.
+ */
+
+#include <gtest/gtest.h>
+#include <renderstate/OffscreenBufferPool.h>
+
+#include <unit_tests/TestUtils.h>
+
+using namespace android;
+using namespace android::uirenderer;
+
+TEST(OffscreenBuffer, computeIdealDimension) {
+ EXPECT_EQ(64u, OffscreenBuffer::computeIdealDimension(1));
+ EXPECT_EQ(64u, OffscreenBuffer::computeIdealDimension(31));
+ EXPECT_EQ(64u, OffscreenBuffer::computeIdealDimension(33));
+ EXPECT_EQ(64u, OffscreenBuffer::computeIdealDimension(64));
+ EXPECT_EQ(1024u, OffscreenBuffer::computeIdealDimension(1000));
+}
+
+TEST(OffscreenBuffer, construct) {
+ TestUtils::runOnRenderThread([] (renderthread::RenderThread& thread) {
+ OffscreenBuffer layer(thread.renderState(), Caches::getInstance(), 49u, 149u);
+ EXPECT_EQ(49u, layer.viewportWidth);
+ EXPECT_EQ(149u, layer.viewportHeight);
+
+ EXPECT_EQ(64u, layer.texture.width);
+ EXPECT_EQ(192u, layer.texture.height);
+
+ EXPECT_EQ(64u * 192u * 4u, layer.getSizeInBytes());
+ });
+}
+
+TEST(OffscreenBufferPool, construct) {
+ TestUtils::runOnRenderThread([] (renderthread::RenderThread& thread) {
+ OffscreenBufferPool pool;
+ EXPECT_EQ(0u, pool.getCount()) << "pool must be created empty";
+ EXPECT_EQ(0u, pool.getSize()) << "pool must be created empty";
+ EXPECT_EQ((uint32_t) Properties::layerPoolSize, pool.getMaxSize())
+ << "pool must read size from Properties";
+ });
+
+}
+
+TEST(OffscreenBufferPool, getPutClear) {
+ TestUtils::runOnRenderThread([] (renderthread::RenderThread& thread) {
+ OffscreenBufferPool pool;
+
+ auto layer = pool.get(thread.renderState(), 100u, 200u);
+ EXPECT_EQ(100u, layer->viewportWidth);
+ EXPECT_EQ(200u, layer->viewportHeight);
+
+ ASSERT_LT(layer->getSizeInBytes(), pool.getMaxSize());
+
+ pool.putOrDelete(layer);
+ ASSERT_EQ(layer->getSizeInBytes(), pool.getSize());
+
+ auto layer2 = pool.get(thread.renderState(), 102u, 202u);
+ EXPECT_EQ(layer, layer2) << "layer should be recycled";
+ ASSERT_EQ(0u, pool.getSize()) << "pool should have been emptied by removing only layer";
+
+ pool.putOrDelete(layer);
+ EXPECT_EQ(1u, pool.getCount());
+ pool.clear();
+ EXPECT_EQ(0u, pool.getSize());
+ EXPECT_EQ(0u, pool.getCount());
+ });
+}
+
+TEST(OffscreenBufferPool, resize) {
+ TestUtils::runOnRenderThread([] (renderthread::RenderThread& thread) {
+ OffscreenBufferPool pool;
+
+ auto layer = pool.get(thread.renderState(), 64u, 64u);
+
+ // resize in place
+ ASSERT_EQ(layer, pool.resize(layer, 60u, 55u));
+ EXPECT_EQ(60u, layer->viewportWidth);
+ EXPECT_EQ(55u, layer->viewportHeight);
+ EXPECT_EQ(64u, layer->texture.width);
+ EXPECT_EQ(64u, layer->texture.height);
+
+ // resized to use different object in pool
+ auto layer2 = pool.get(thread.renderState(), 128u, 128u);
+ pool.putOrDelete(layer2);
+ ASSERT_EQ(1u, pool.getCount());
+ ASSERT_EQ(layer2, pool.resize(layer, 120u, 125u));
+ EXPECT_EQ(120u, layer2->viewportWidth);
+ EXPECT_EQ(125u, layer2->viewportHeight);
+ EXPECT_EQ(128u, layer2->texture.width);
+ EXPECT_EQ(128u, layer2->texture.height);
+
+ // original allocation now only thing in pool
+ EXPECT_EQ(1u, pool.getCount());
+ EXPECT_EQ(layer->getSizeInBytes(), pool.getSize());
+ });
+}
+
+TEST(OffscreenBufferPool, putAndDestroy) {
+ TestUtils::runOnRenderThread([] (renderthread::RenderThread& thread) {
+ OffscreenBufferPool pool;
+ // layer too big to return to the pool
+ // Note: this relies on the fact that the pool won't reject based on max texture size
+ auto hugeLayer = pool.get(thread.renderState(), pool.getMaxSize() / 64, 64);
+ EXPECT_GT(hugeLayer->getSizeInBytes(), pool.getMaxSize());
+ pool.putOrDelete(hugeLayer);
+ EXPECT_EQ(0u, pool.getCount()); // failed to put (so was destroyed instead)
+ });
+}
diff --git a/libs/hwui/unit_tests/PathParserTests.cpp b/libs/hwui/unit_tests/PathParserTests.cpp
index 60ea219..c99d7b0 100644
--- a/libs/hwui/unit_tests/PathParserTests.cpp
+++ b/libs/hwui/unit_tests/PathParserTests.cpp
@@ -164,16 +164,37 @@
};
+struct StringPath {
+ const char* stringPath;
+ bool isValid;
+};
+
+const StringPath sStringPaths[] = {
+ {"3e...3", false},
+ {"L.M.F.A.O", false},
+ {"m 1 1", true},
+ {"z", true},
+ {"1-2e34567", false}
+};
+
TEST(PathParser, parseStringForData) {
for (TestData testData: sTestDataSet) {
+ PathParser::ParseResult result;
// Test generated path data against the given data.
PathData pathData;
size_t length = strlen(testData.pathString);
- PathParser::getPathDataFromString(&pathData, testData.pathString, length);
- PathParser::dump(pathData);
+ PathParser::getPathDataFromString(&pathData, &result, testData.pathString, length);
EXPECT_EQ(testData.pathData, pathData);
}
+ for (StringPath stringPath : sStringPaths) {
+ PathParser::ParseResult result;
+ PathData pathData;
+ SkPath skPath;
+ PathParser::getPathDataFromString(&pathData, &result,
+ stringPath.stringPath, strlen(stringPath.stringPath));
+ EXPECT_EQ(stringPath.isValid, !result.failureOccurred);
+ }
}
TEST(PathParser, createSkPathFromPathData) {
@@ -188,21 +209,25 @@
TEST(PathParser, parseStringForSkPath) {
for (TestData testData: sTestDataSet) {
+ PathParser::ParseResult result;
size_t length = strlen(testData.pathString);
// Check the return value as well as the SkPath generated.
SkPath actualPath;
- bool hasValidData = PathParser::parseStringForSkPath(&actualPath, testData.pathString,
- length);
+ PathParser::parseStringForSkPath(&actualPath, &result, testData.pathString, length);
+ bool hasValidData = !result.failureOccurred;
EXPECT_EQ(hasValidData, testData.pathData.verbs.size() > 0);
SkPath expectedPath;
testData.skPathLamda(&expectedPath);
EXPECT_EQ(expectedPath, actualPath);
}
- SkPath path;
- EXPECT_FALSE(PathParser::parseStringForSkPath(&path, "l", 1));
- EXPECT_FALSE(PathParser::parseStringForSkPath(&path, "1 1", 3));
- EXPECT_FALSE(PathParser::parseStringForSkPath(&path, "LMFAO", 5));
- EXPECT_TRUE(PathParser::parseStringForSkPath(&path, "m1 1", 4));
+
+ for (StringPath stringPath : sStringPaths) {
+ PathParser::ParseResult result;
+ SkPath skPath;
+ PathParser::parseStringForSkPath(&skPath, &result, stringPath.stringPath,
+ strlen(stringPath.stringPath));
+ EXPECT_EQ(stringPath.isValid, !result.failureOccurred);
+ }
}
}; // namespace uirenderer
diff --git a/libs/hwui/unit_tests/TestUtils.h b/libs/hwui/unit_tests/TestUtils.h
index 23e5398..efa28ae 100644
--- a/libs/hwui/unit_tests/TestUtils.h
+++ b/libs/hwui/unit_tests/TestUtils.h
@@ -109,9 +109,11 @@
static sp<RenderNode> createNode(int left, int top, int right, int bottom,
PropSetupCallback propSetupCallback = nullptr) {
+#if HWUI_NULL_GPU
// if RenderNodes are being sync'd/used, device info will be needed, since
// DeviceInfo::maxTextureSize() affects layer property
DeviceInfo::initialize();
+#endif
sp<RenderNode> node = new RenderNode();
node->mutateStagingProperties().setLeftTopRightBottom(left, top, right, bottom);
diff --git a/libs/hwui/utils/Macros.h b/libs/hwui/utils/Macros.h
index 5ca9083..ccf2287 100644
--- a/libs/hwui/utils/Macros.h
+++ b/libs/hwui/utils/Macros.h
@@ -35,4 +35,7 @@
static_assert(std::is_standard_layout<Type>::value, \
#Type " must have standard layout")
+#define WARN_UNUSED_RESULT \
+ __attribute__((warn_unused_result))
+
#endif /* MACROS_H */
diff --git a/packages/DocumentsUI/src/com/android/documentsui/Events.java b/packages/DocumentsUI/src/com/android/documentsui/Events.java
index 49dae3d..1b5b60de 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/Events.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/Events.java
@@ -117,6 +117,8 @@
public MotionInputEvent(MotionEvent event, RecyclerView view) {
mEvent = event;
mView = view;
+
+ // Consider determining position lazily as an optimization.
View child = mView.findChildViewUnder(mEvent.getX(), mEvent.getY());
mPosition = (child != null)
? mView.getChildAdapterPosition(child)
diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/MultiSelectManager.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/MultiSelectManager.java
index 9eafcc3..65e1a28 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/MultiSelectManager.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/MultiSelectManager.java
@@ -41,10 +41,9 @@
import android.view.MotionEvent;
import android.view.View;
-import com.android.documentsui.Events;
-import com.android.documentsui.R;
import com.android.documentsui.Events.InputEvent;
import com.android.documentsui.Events.MotionInputEvent;
+import com.android.documentsui.R;
import java.util.ArrayList;
import java.util.Collections;
@@ -88,9 +87,7 @@
* @param mode Selection mode
*/
public MultiSelectManager(final RecyclerView recyclerView, int mode) {
- this(recyclerView.getAdapter(), mode);
-
- mEnvironment = new RuntimeSelectionEnvironment(recyclerView);
+ this(recyclerView.getAdapter(), new RuntimeSelectionEnvironment(recyclerView), mode);
if (mode == MODE_MULTIPLE) {
mBandManager = new BandController();
@@ -137,16 +134,15 @@
/**
* Constructs a new instance with {@code adapter} and {@code helper}.
+ * @param runtimeSelectionEnvironment
* @hide
*/
@VisibleForTesting
- MultiSelectManager(Adapter<?> adapter, int mode) {
- checkNotNull(adapter, "'adapter' cannot be null.");
-
+ MultiSelectManager(Adapter<?> adapter, SelectionEnvironment environment, int mode) {
+ mAdapter = checkNotNull(adapter, "'adapter' cannot be null.");
+ mEnvironment = checkNotNull(environment, "'environment' cannot be null.");
mSingleSelect = mode == MODE_SINGLE;
- mAdapter = adapter;
-
mAdapter.registerAdapterDataObserver(
new AdapterDataObserver() {
@@ -880,7 +876,7 @@
void focusItem(int position);
}
- /** RvFacade implementation backed by good ol' RecyclerView. */
+ /** Recycler view facade implementation backed by good ol' RecyclerView. */
private static final class RuntimeSelectionEnvironment implements SelectionEnvironment {
private final RecyclerView mView;
@@ -1960,11 +1956,50 @@
return false;
}
- int target = RecyclerView.NO_POSITION;
+ // Here we unpack information from the event and pass it to an more
+ // easily tested method....basically eliminating the need to synthesize
+ // events and views and so on in our tests.
+ int position = findTargetPosition(view, keyCode);
+ if (position == RecyclerView.NO_POSITION) {
+ // If there is no valid navigation target, don't handle the keypress.
+ return false;
+ }
+
+ return attemptChangePosition(position, event.isShiftPressed());
+ }
+
+ @VisibleForTesting
+ boolean attemptChangePosition(int targetPosition, boolean isShiftPressed) {
+ // Focus the new file.
+ mEnvironment.focusItem(targetPosition);
+
+ if (isShiftPressed) {
+ if (!hasSelection()) {
+ // If there is no selection, start a selection when the user presses shift-arrow.
+ toggleSelection(targetPosition);
+ } else if (!mSingleSelect) {
+ mRanger.snapSelection(targetPosition);
+ notifySelectionChanged();
+ } else {
+ // We're in single select and have an existing selection.
+ // Our best guess as to what the user would expect is to advance the selection.
+ clearSelection();
+ toggleSelection(targetPosition);
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns the adapter position that the key combo is targeted at.
+ */
+ private int findTargetPosition(View view, int keyCode) {
+ int position = RecyclerView.NO_POSITION;
if (keyCode == KeyEvent.KEYCODE_MOVE_HOME) {
- target = 0;
+ position = 0;
} else if (keyCode == KeyEvent.KEYCODE_MOVE_END) {
- target = mAdapter.getItemCount() - 1;
+ position = mAdapter.getItemCount() - 1;
} else {
// Find a navigation target based on the arrow key that the user pressed. Ignore
// navigation targets that aren't items in the recycler view.
@@ -1988,30 +2023,10 @@
// TargetView can be null, for example, if the user pressed <down> at the bottom of
// the list.
if (targetView != null) {
- target = mEnvironment.getAdapterPositionForChildView(targetView);
+ position = mEnvironment.getAdapterPositionForChildView(targetView);
}
}
}
-
- if (target == RecyclerView.NO_POSITION) {
- // If there is no valid navigation target, don't handle the keypress.
- return false;
- }
-
- // Focus the new file.
- mEnvironment.focusItem(target);
-
- if (event.isShiftPressed()) {
- if (!hasSelection()) {
- // If there is no selection, start a selection when the user presses shift-arrow.
- toggleSelection(mEnvironment.getAdapterPositionForChildView(view));
- }
-
- mRanger.snapSelection(target);
- notifySelectionChanged();
- }
-
- return true;
+ return position;
}
-
}
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/MultiSelectManagerTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/MultiSelectManagerTest.java
index 24f5c9e..d1ce564 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/MultiSelectManagerTest.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/MultiSelectManagerTest.java
@@ -23,7 +23,6 @@
import android.view.ViewGroup;
import com.android.documentsui.TestInputEvent;
-import com.android.documentsui.dirlist.MultiSelectManager;
import com.android.documentsui.dirlist.MultiSelectManager.Selection;
import org.mockito.Mockito;
@@ -49,11 +48,13 @@
private MultiSelectManager mManager;
private TestAdapter mAdapter;
private TestCallback mCallback;
+ private TestSelectionEnvironment mEnv;
public void setUp() throws Exception {
mAdapter = new TestAdapter(items);
mCallback = new TestCallback();
- mManager = new MultiSelectManager(mAdapter, MultiSelectManager.MODE_MULTIPLE);
+ mEnv = new TestSelectionEnvironment();
+ mManager = new MultiSelectManager(mAdapter, mEnv, MultiSelectManager.MODE_MULTIPLE);
mManager.addCallback(mCallback);
}
@@ -171,7 +172,7 @@
}
public void testSingleSelectMode() {
- mManager = new MultiSelectManager(mAdapter, MultiSelectManager.MODE_SINGLE);
+ mManager = new MultiSelectManager(mAdapter, mEnv, MultiSelectManager.MODE_SINGLE);
mManager.addCallback(mCallback);
longPress(20);
tap(13);
@@ -179,13 +180,21 @@
}
public void testSingleSelectMode_ShiftTap() {
- mManager = new MultiSelectManager(mAdapter, MultiSelectManager.MODE_SINGLE);
+ mManager = new MultiSelectManager(mAdapter, mEnv, MultiSelectManager.MODE_SINGLE);
mManager.addCallback(mCallback);
longPress(13);
shiftTap(20);
assertSelection(20);
}
+ public void testSingleSelectMode_ShiftDoesNotExtendSelection() {
+ mManager = new MultiSelectManager(mAdapter, mEnv, MultiSelectManager.MODE_SINGLE);
+ mManager.addCallback(mCallback);
+ longPress(20);
+ keyToPosition(22, true);
+ assertSelection(22);
+ }
+
public void testProvisionalSelection() {
Selection s = mManager.getSelection();
assertSelection();
@@ -235,6 +244,10 @@
mManager.onSingleTapUp(TestInputEvent.shiftClick(position));
}
+ private void keyToPosition(int position, boolean shift) {
+ mManager.attemptChangePosition(position, shift);
+ }
+
private void assertSelected(int... expected) {
for (int i = 0; i < expected.length; i++) {
Selection selection = mManager.getSelection();
@@ -290,11 +303,8 @@
private static final class TestHolder extends RecyclerView.ViewHolder {
// each data item is just a string in this case
- public View view;
- public String string;
public TestHolder(View view) {
super(view);
- this.view = view;
}
}
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/TestSelectionEnvironment.java b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/TestSelectionEnvironment.java
new file mode 100644
index 0000000..b4324a8
--- /dev/null
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/TestSelectionEnvironment.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2015 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.documentsui.dirlist;
+
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.support.v7.widget.RecyclerView.OnScrollListener;
+import android.view.View;
+
+import com.android.documentsui.dirlist.MultiSelectManager.SelectionEnvironment;
+
+public class TestSelectionEnvironment implements SelectionEnvironment {
+
+ @Override
+ public void showBand(Rect rect) {
+ }
+
+ @Override
+ public void hideBand() {
+ }
+
+ @Override
+ public void addOnScrollListener(OnScrollListener listener) {
+ }
+
+ @Override
+ public void removeOnScrollListener(OnScrollListener listener) {
+ }
+
+ @Override
+ public void scrollBy(int dy) {
+ }
+
+ @Override
+ public int getHeight() {
+ return 0;
+ }
+
+ @Override
+ public void invalidateView() {
+ }
+
+ @Override
+ public void runAtNextFrame(Runnable r) {
+ }
+
+ @Override
+ public void removeCallback(Runnable r) {
+ }
+
+ @Override
+ public Point createAbsolutePoint(Point relativePoint) {
+ return null;
+ }
+
+ @Override
+ public Rect getAbsoluteRectForChildViewAt(int index) {
+ return null;
+ }
+
+ @Override
+ public int getAdapterPositionAt(int index) {
+ return 0;
+ }
+
+ @Override
+ public int getAdapterPositionForChildView(View view) {
+ return 0;
+ }
+
+ @Override
+ public int getColumnCount() {
+ return 0;
+ }
+
+ @Override
+ public int getRowCount() {
+ return 0;
+ }
+
+ @Override
+ public int getChildCount() {
+ return 0;
+ }
+
+ @Override
+ public int getVisibleChildCount() {
+ return 0;
+ }
+
+ @Override
+ public void focusItem(int position) {
+ }
+}
diff --git a/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDatabase.java b/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDatabase.java
index 0f31e2c..e3be534 100644
--- a/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDatabase.java
+++ b/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDatabase.java
@@ -16,22 +16,20 @@
package com.android.mtp;
+import static com.android.mtp.MtpDatabaseConstants.*;
+
import android.content.ContentValues;
import android.content.Context;
import android.content.res.Resources;
import android.database.Cursor;
-import android.database.DatabaseUtils;
-import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteOpenHelper;
-import android.database.sqlite.SQLiteQueryBuilder;
import android.mtp.MtpObjectInfo;
import android.provider.DocumentsContract.Document;
import android.provider.DocumentsContract.Root;
-import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
-import java.util.Objects;
+import java.util.HashMap;
+import java.util.Map;
/**
* Database for MTP objects.
@@ -46,182 +44,76 @@
* remembers the map of document ID and object handle, and remaps new object handle with document ID
* by comparing the directory structure and object name.
*
+ * To start putting documents into the database, the client needs to call
+ * {@link #startAddingChildDocuments(String)} with the parent document ID. Also it needs to call
+ * {@link #stopAddingChildDocuments(String)} after putting all child documents to the database.
+ * (All explanations are same for root documents)
+ *
+ * database.startAddingChildDocuments();
+ * database.putChildDocuments();
+ * database.stopAddingChildDocuments();
+ *
+ * To update the existing documents, the client code can repeat to call the three methods again.
+ * The newly added rows update corresponding existing rows that have same MTP identifier like
+ * objectHandle.
+ *
+ * The client can call putChildDocuments multiple times to add documents by chunk, but it needs to
+ * put all documents under the parent before calling stopAddingChildDocuments. Otherwise missing
+ * documents are regarded as deleted, and will be removed from the database.
+ *
+ * If the client calls clearMtpIdentifier(), it clears MTP identifier in the database. In this case,
+ * the database tries to find corresponding rows by using document's name instead of MTP identifier
+ * at the next update cycle.
+ *
* TODO: Remove @VisibleForTesting annotation when we start to use this class.
* TODO: Improve performance by SQL optimization.
*/
@VisibleForTesting
class MtpDatabase {
- private static final int VERSION = 1;
- private static final String NAME = "mtp";
-
- /**
- * Table representing documents including root documents.
- */
- private static final String TABLE_DOCUMENTS = "Documents";
-
- /**
- * Table containing additional information only available for root documents.
- * The table uses same primary keys with corresponding documents.
- */
- private static final String TABLE_ROOT_EXTRA = "RootExtra";
-
- /**
- * View to join Documents and RootExtra tables to provide roots information.
- */
- private static final String VIEW_ROOTS = "Roots";
-
- static final String COLUMN_DEVICE_ID = "device_id";
- static final String COLUMN_STORAGE_ID = "storage_id";
- static final String COLUMN_OBJECT_HANDLE = "object_handle";
- static final String COLUMN_PARENT_DOCUMENT_ID = "parent_document_id";
- static final String COLUMN_ROW_STATE = "row_state";
-
- /**
- * The state represents that the row has a valid object handle.
- */
- static final int ROW_STATE_MAPPED = 0;
-
- /**
- * The state represents that the object handle was cleared because the MTP session closed.
- * External application can still fetch the unmapped documents. If the external application
- * tries to open an unmapped document, the provider resolves the document with new object handle
- * ahead.
- */
- static final int ROW_STATE_UNMAPPED = 1;
-
- /**
- * The state represents the raw has a valid object handle but it may be going to be merged into
- * another unmapped row. After fetching all documents under the parent, the database tries to
- * map the mapping document and the unmapped document in order to keep old document ID alive.
- */
- static final int ROW_STATE_MAPPING = 2;
-
- private static final String SELECTION_DOCUMENT_ID = Document.COLUMN_DOCUMENT_ID + " = ?";
- private static final String SELECTION_ROOT_ID = Root.COLUMN_ROOT_ID + " = ?";
- private static final String SELECTION_ROOT_DOCUMENTS =
- COLUMN_DEVICE_ID + " = ? AND " + COLUMN_PARENT_DOCUMENT_ID + " IS NULL";
- private static final String SELECTION_CHILD_DOCUMENTS = COLUMN_PARENT_DOCUMENT_ID + " = ?";
-
- static class ParentNotFoundException extends Exception {}
-
- private static class OpenHelper extends SQLiteOpenHelper {
- private static final String QUERY_CREATE_DOCUMENTS =
- "CREATE TABLE " + TABLE_DOCUMENTS + " (" +
- Document.COLUMN_DOCUMENT_ID +
- " INTEGER PRIMARY KEY AUTOINCREMENT," +
- COLUMN_DEVICE_ID + " INTEGER NOT NULL," +
- COLUMN_STORAGE_ID + " INTEGER," +
- COLUMN_OBJECT_HANDLE + " INTEGER," +
- COLUMN_PARENT_DOCUMENT_ID + " INTEGER," +
- COLUMN_ROW_STATE + " INTEGER NOT NULL," +
- Document.COLUMN_MIME_TYPE + " TEXT," +
- Document.COLUMN_DISPLAY_NAME + " TEXT NOT NULL," +
- Document.COLUMN_SUMMARY + " TEXT," +
- Document.COLUMN_LAST_MODIFIED + " INTEGER," +
- Document.COLUMN_ICON + " INTEGER," +
- Document.COLUMN_FLAGS + " INTEGER NOT NULL," +
- Document.COLUMN_SIZE + " INTEGER NOT NULL);";
-
- private static final String QUERY_CREATE_ROOT_EXTRA =
- "CREATE TABLE " + TABLE_ROOT_EXTRA + " (" +
- Root.COLUMN_ROOT_ID + " INTEGER PRIMARY KEY," +
- Root.COLUMN_FLAGS + " INTEGER NOT NULL," +
- Root.COLUMN_AVAILABLE_BYTES + " INTEGER NOT NULL," +
- Root.COLUMN_CAPACITY_BYTES + " INTEGER NOT NULL," +
- Root.COLUMN_MIME_TYPES + " TEXT NOT NULL);";
-
- /**
- * Creates a view to join Documents table and RootExtra table on their primary keys to
- * provide DocumentContract.Root equivalent information.
- */
- private static final String QUERY_CREATE_VIEW_ROOTS =
- "CREATE VIEW " + VIEW_ROOTS + " AS SELECT " +
- TABLE_DOCUMENTS + "." + Document.COLUMN_DOCUMENT_ID + " AS " +
- Root.COLUMN_ROOT_ID + "," +
- TABLE_ROOT_EXTRA + "." + Root.COLUMN_FLAGS + "," +
- TABLE_DOCUMENTS + "." + Document.COLUMN_ICON + " AS " +
- Root.COLUMN_ICON + "," +
- TABLE_DOCUMENTS + "." + Document.COLUMN_DISPLAY_NAME + " AS " +
- Root.COLUMN_TITLE + "," +
- TABLE_DOCUMENTS + "." + Document.COLUMN_SUMMARY + " AS " +
- Root.COLUMN_SUMMARY + "," +
- TABLE_DOCUMENTS + "." + Document.COLUMN_DOCUMENT_ID + " AS " +
- Root.COLUMN_DOCUMENT_ID + "," +
- TABLE_ROOT_EXTRA + "." + Root.COLUMN_AVAILABLE_BYTES + "," +
- TABLE_ROOT_EXTRA + "." + Root.COLUMN_CAPACITY_BYTES + "," +
- TABLE_ROOT_EXTRA + "." + Root.COLUMN_MIME_TYPES + "," +
- TABLE_DOCUMENTS + "." + COLUMN_ROW_STATE +
- " FROM " + TABLE_DOCUMENTS + " INNER JOIN " + TABLE_ROOT_EXTRA +
- " ON " +
- COLUMN_PARENT_DOCUMENT_ID + " IS NULL AND " +
- TABLE_DOCUMENTS + "." + Document.COLUMN_DOCUMENT_ID +
- "=" +
- TABLE_ROOT_EXTRA + "." + Root.COLUMN_ROOT_ID;
-
- public OpenHelper(Context context) {
- super(context, NAME, null, VERSION);
- }
-
- @Override
- public void onCreate(SQLiteDatabase db) {
- db.execSQL(QUERY_CREATE_DOCUMENTS);
- db.execSQL(QUERY_CREATE_ROOT_EXTRA);
- db.execSQL(QUERY_CREATE_VIEW_ROOTS);
- }
-
- @Override
- public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
- throw new UnsupportedOperationException();
- }
- }
-
- private final SQLiteDatabase mDatabase;
+ private final MtpDatabaseInternal mDatabase;
+ private final Map<String, Integer> mMappingMode = new HashMap<>();
@VisibleForTesting
MtpDatabase(Context context) {
- final OpenHelper helper = new OpenHelper(context);
- mDatabase = helper.getWritableDatabase();
- }
-
- @VisibleForTesting
- static void deleteDatabase(Context context) {
- SQLiteDatabase.deleteDatabase(context.getDatabasePath(NAME));
+ mDatabase = new MtpDatabaseInternal(context);
}
@VisibleForTesting
Cursor queryRoots(String[] columnNames) {
- return mDatabase.query(
- VIEW_ROOTS,
- columnNames,
- COLUMN_ROW_STATE + " IN (?, ?)",
- strings(ROW_STATE_MAPPED, ROW_STATE_UNMAPPED),
- null,
- null,
- null);
+ return mDatabase.queryRoots(columnNames);
}
@VisibleForTesting
Cursor queryRootDocuments(String[] columnNames) {
- return mDatabase.query(
- TABLE_DOCUMENTS,
- columnNames,
- COLUMN_ROW_STATE + " IN (?, ?)",
- strings(ROW_STATE_MAPPED, ROW_STATE_UNMAPPED),
- null,
- null,
- null);
+ return mDatabase.queryRootDocuments(columnNames);
}
@VisibleForTesting
Cursor queryChildDocuments(String[] columnNames, String parentDocumentId) {
- return mDatabase.query(
- TABLE_DOCUMENTS,
- columnNames,
- COLUMN_ROW_STATE + " IN (?, ?) AND " + COLUMN_PARENT_DOCUMENT_ID + " = ?",
- strings(ROW_STATE_MAPPED, ROW_STATE_UNMAPPED, parentDocumentId),
- null,
- null,
- null);
+ return mDatabase.queryChildDocuments(columnNames, parentDocumentId);
+ }
+
+ @VisibleForTesting
+ void startAddingRootDocuments(int deviceId) {
+ final String mappingStateKey = getRootDocumentsMappingStateKey(deviceId);
+ if (mMappingMode.containsKey(mappingStateKey)) {
+ throw new Error("Mapping for the root has already started.");
+ }
+ mMappingMode.put(
+ mappingStateKey,
+ mDatabase.startAddingDocuments(
+ SELECTION_ROOT_DOCUMENTS, Integer.toString(deviceId)));
+ }
+
+ @VisibleForTesting
+ void startAddingChildDocuments(String parentDocumentId) {
+ final String mappingStateKey = getChildDocumentsMappingStateKey(parentDocumentId);
+ if (mMappingMode.containsKey(mappingStateKey)) {
+ throw new Error("Mapping for the root has already started.");
+ }
+ mMappingMode.put(
+ mappingStateKey,
+ mDatabase.startAddingDocuments(SELECTION_CHILD_DOCUMENTS, parentDocumentId));
}
@VisibleForTesting
@@ -236,20 +128,37 @@
valuesList[i] = new ContentValues();
getRootDocumentValues(valuesList[i], resources, roots[i]);
}
- final long[] documentIds =
- putDocuments(valuesList, SELECTION_ROOT_DOCUMENTS, Integer.toString(deviceId));
+ boolean heuristic;
+ String mapColumn;
+ switch (mMappingMode.get(getRootDocumentsMappingStateKey(deviceId))) {
+ case MAP_BY_MTP_IDENTIFIER:
+ heuristic = false;
+ mapColumn = COLUMN_STORAGE_ID;
+ break;
+ case MAP_BY_NAME:
+ heuristic = true;
+ mapColumn = Document.COLUMN_DISPLAY_NAME;
+ break;
+ default:
+ throw new Error("Unexpected map mode.");
+ }
+ final long[] documentIds = mDatabase.putDocuments(
+ valuesList,
+ SELECTION_ROOT_DOCUMENTS,
+ Integer.toString(deviceId),
+ heuristic,
+ mapColumn);
final ContentValues values = new ContentValues();
int i = 0;
for (final MtpRoot root : roots) {
// Use the same value for the root ID and the corresponding document ID.
values.put(Root.COLUMN_ROOT_ID, documentIds[i++]);
- values.put(Root.COLUMN_FLAGS,
- Root.FLAG_SUPPORTS_IS_CHILD |
- Root.FLAG_SUPPORTS_CREATE);
+ values.put(
+ Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_IS_CHILD | Root.FLAG_SUPPORTS_CREATE);
values.put(Root.COLUMN_AVAILABLE_BYTES, root.mFreeSpace);
values.put(Root.COLUMN_CAPACITY_BYTES, root.mMaxCapacity);
values.put(Root.COLUMN_MIME_TYPES, "");
- mDatabase.insert(TABLE_ROOT_EXTRA, null, values);
+ mDatabase.putRootExtra(values);
}
mDatabase.setTransactionSuccessful();
} finally {
@@ -264,172 +173,72 @@
valuesList[i] = new ContentValues();
getChildDocumentValues(valuesList[i], deviceId, parentId, documents[i]);
}
- putDocuments(valuesList, SELECTION_CHILD_DOCUMENTS, parentId);
- }
-
- /**
- * Clears MTP related identifier.
- * It clears MTP's object handle and storage ID that are not stable over MTP sessions and mark
- * the all documents as 'unmapped'. It also remove 'mapping' rows as mapping is cancelled now.
- */
- @VisibleForTesting
- void clearMapping() {
- mDatabase.beginTransaction();
- try {
- deleteDocumentsAndRoots(COLUMN_ROW_STATE + " = ?", strings(ROW_STATE_MAPPING));
- final ContentValues values = new ContentValues();
- values.putNull(COLUMN_OBJECT_HANDLE);
- values.putNull(COLUMN_STORAGE_ID);
- values.put(COLUMN_ROW_STATE, ROW_STATE_UNMAPPED);
- mDatabase.update(TABLE_DOCUMENTS, values, null, null);
- mDatabase.setTransactionSuccessful();
- } finally {
- mDatabase.endTransaction();
+ boolean heuristic;
+ String mapColumn;
+ switch (mMappingMode.get(getChildDocumentsMappingStateKey(parentId))) {
+ case MAP_BY_MTP_IDENTIFIER:
+ heuristic = false;
+ mapColumn = COLUMN_STORAGE_ID;
+ break;
+ case MAP_BY_NAME:
+ heuristic = true;
+ mapColumn = Document.COLUMN_DISPLAY_NAME;
+ break;
+ default:
+ throw new Error("Unexpected map mode.");
}
+ mDatabase.putDocuments(
+ valuesList, SELECTION_CHILD_DOCUMENTS, parentId, heuristic, mapColumn);
}
@VisibleForTesting
- void resolveRootDocuments(int deviceId) {
- resolveDocuments(SELECTION_ROOT_DOCUMENTS, Integer.toString(deviceId));
+ void clearMapping() {
+ mDatabase.clearMapping();
+ mMappingMode.clear();
}
@VisibleForTesting
- void resolveChildDocuments(String parentId) {
- resolveDocuments(SELECTION_CHILD_DOCUMENTS, parentId);
+ void stopAddingRootDocuments(int deviceId) {
+ final String mappingModeKey = getRootDocumentsMappingStateKey(deviceId);
+ switch (mMappingMode.get(mappingModeKey)) {
+ case MAP_BY_MTP_IDENTIFIER:
+ mDatabase.stopAddingDocuments(
+ SELECTION_ROOT_DOCUMENTS,
+ Integer.toString(deviceId),
+ COLUMN_STORAGE_ID);
+ break;
+ case MAP_BY_NAME:
+ mDatabase.stopAddingDocuments(
+ SELECTION_ROOT_DOCUMENTS,
+ Integer.toString(deviceId),
+ Document.COLUMN_DISPLAY_NAME);
+ break;
+ default:
+ throw new Error("Unexpected mapping state.");
+ }
+ mMappingMode.remove(mappingModeKey);
}
- /**
- * Puts the documents into the database.
- * If the database found another unmapped document that shares the same name and parent,
- * the document may be merged into the unmapped document. In that case, the database marks the
- * root as 'mapping' and wait for {@link #resolveRootDocuments(int)} is invoked.
- * @param valuesList Values that are stored in the database.
- * @param selection SQL where closure to select rows that shares the same parent.
- * @param arg Argument for selection SQL.
- * @return List of Document ID inserted to the table.
- */
- private long[] putDocuments(ContentValues[] valuesList, String selection, String arg) {
- mDatabase.beginTransaction();
- try {
- final long[] documentIds = new long[valuesList.length];
- int i = 0;
- for (final ContentValues values : valuesList) {
- final String displayName =
- values.getAsString(Document.COLUMN_DISPLAY_NAME);
- final long numUnmapped = DatabaseUtils.queryNumEntries(
- mDatabase,
- TABLE_DOCUMENTS,
- selection + " AND " +
- COLUMN_ROW_STATE + " = ? AND " +
- Document.COLUMN_DISPLAY_NAME + " = ?",
- strings(arg, ROW_STATE_UNMAPPED, displayName));
- if (numUnmapped != 0) {
- values.put(COLUMN_ROW_STATE, ROW_STATE_MAPPING);
- }
- // Document ID is a primary integer key of the table. So the returned row IDs should
- // be same with the document ID.
- documentIds[i++] = mDatabase.insert(TABLE_DOCUMENTS, null, values);
- }
-
- mDatabase.setTransactionSuccessful();
- return documentIds;
- } finally {
- mDatabase.endTransaction();
+ @VisibleForTesting
+ void stopAddingChildDocuments(String parentId) {
+ final String mappingModeKey = getChildDocumentsMappingStateKey(parentId);
+ switch (mMappingMode.get(mappingModeKey)) {
+ case MAP_BY_MTP_IDENTIFIER:
+ mDatabase.stopAddingDocuments(
+ SELECTION_CHILD_DOCUMENTS,
+ parentId,
+ COLUMN_OBJECT_HANDLE);
+ break;
+ case MAP_BY_NAME:
+ mDatabase.stopAddingDocuments(
+ SELECTION_CHILD_DOCUMENTS,
+ parentId,
+ Document.COLUMN_DISPLAY_NAME);
+ break;
+ default:
+ throw new Error("Unexpected mapping state.");
}
- }
-
- /**
- * Maps 'unmapped' document and 'mapping' document that don't have document but shares the same
- * name.
- * If the database does not find corresponding 'mapping' document, it just removes 'unmapped'
- * document from the database.
- * @param selection Query to select rows for resolving.
- * @param arg Argument for selection SQL.
- */
- private void resolveDocuments(String selection, String arg) {
- mDatabase.beginTransaction();
- try {
- // Get 1-to-1 mapping of unmapped document and mapping document.
- final String unmappedIdQuery = createStateFilter(
- ROW_STATE_UNMAPPED, Document.COLUMN_DOCUMENT_ID);
- final String mappingIdQuery = createStateFilter(
- ROW_STATE_MAPPING, Document.COLUMN_DOCUMENT_ID);
- // SQL should be like:
- // SELECT group_concat(CASE WHEN raw_state = 1 THEN document_id ELSE NULL END),
- // group_concat(CASE WHEN raw_state = 2 THEN document_id ELSE NULL END)
- // WHERE device_id = ? AND parent_document_id IS NULL
- // GROUP BY display_name
- // HAVING count(CASE WHEN raw_state = 1 THEN document_id ELSE NULL END) = 1 AND
- // count(CASE WHEN raw_state = 2 THEN document_id ELSE NULL END) = 1
- final Cursor mergingCursor = mDatabase.query(
- TABLE_DOCUMENTS,
- new String[] {
- "group_concat(" + unmappedIdQuery + ")",
- "group_concat(" + mappingIdQuery + ")"
- },
- selection,
- strings(arg),
- Document.COLUMN_DISPLAY_NAME,
- "count(" + unmappedIdQuery + ") = 1 AND count(" + mappingIdQuery + ") = 1",
- null);
-
- final ContentValues values = new ContentValues();
- while (mergingCursor.moveToNext()) {
- final String unmappedId = mergingCursor.getString(0);
- final String mappingId = mergingCursor.getString(1);
-
- // Obtain the new values including the latest object handle from mapping row.
- getFirstRow(
- TABLE_DOCUMENTS,
- SELECTION_DOCUMENT_ID,
- new String[] { mappingId },
- values);
- values.remove(Document.COLUMN_DOCUMENT_ID);
- values.put(COLUMN_ROW_STATE, ROW_STATE_MAPPED);
- mDatabase.update(
- TABLE_DOCUMENTS,
- values,
- SELECTION_DOCUMENT_ID,
- new String[] { unmappedId });
-
- getFirstRow(
- TABLE_ROOT_EXTRA,
- SELECTION_ROOT_ID,
- new String[] { mappingId },
- values);
- if (values.size() > 0) {
- values.remove(Root.COLUMN_ROOT_ID);
- mDatabase.update(
- TABLE_ROOT_EXTRA,
- values,
- SELECTION_ROOT_ID,
- new String[] { unmappedId });
- }
-
- // Delete 'mapping' row.
- deleteDocumentsAndRoots(SELECTION_DOCUMENT_ID, new String[] { mappingId });
- }
- mergingCursor.close();
-
- // Delete all unmapped rows that cannot be mapped.
- deleteDocumentsAndRoots(
- COLUMN_ROW_STATE + " = ? AND " + selection,
- strings(ROW_STATE_UNMAPPED, arg));
-
- // The database cannot find old document ID for the mapping rows.
- // Turn the all mapping rows into mapped state, which means the rows become to be
- // valid with new document ID.
- values.clear();
- values.put(COLUMN_ROW_STATE, ROW_STATE_MAPPED);
- mDatabase.update(
- TABLE_DOCUMENTS,
- values,
- COLUMN_ROW_STATE + " = ? AND " + selection,
- strings(ROW_STATE_MAPPING, arg));
- mDatabase.setTransactionSuccessful();
- } finally {
- mDatabase.endTransaction();
- }
+ mMappingMode.remove(mappingModeKey);
}
/**
@@ -445,7 +254,7 @@
values.put(COLUMN_STORAGE_ID, root.mStorageId);
values.putNull(COLUMN_OBJECT_HANDLE);
values.putNull(COLUMN_PARENT_DOCUMENT_ID);
- values.put(COLUMN_ROW_STATE, ROW_STATE_MAPPED);
+ values.put(COLUMN_ROW_STATE, ROW_STATE_VALID);
values.put(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR);
values.put(Document.COLUMN_DISPLAY_NAME, root.getRootName(resources));
values.putNull(Document.COLUMN_SUMMARY);
@@ -482,7 +291,7 @@
values.put(COLUMN_STORAGE_ID, info.getStorageId());
values.put(COLUMN_OBJECT_HANDLE, info.getObjectHandle());
values.put(COLUMN_PARENT_DOCUMENT_ID, parentId);
- values.put(COLUMN_ROW_STATE, ROW_STATE_MAPPED);
+ values.put(COLUMN_ROW_STATE, ROW_STATE_VALID);
values.put(Document.COLUMN_MIME_TYPE, mimeType);
values.put(Document.COLUMN_DISPLAY_NAME, info.getName());
values.putNull(Document.COLUMN_SUMMARY);
@@ -494,73 +303,11 @@
values.put(Document.COLUMN_SIZE, info.getCompressedSize());
}
- /**
- * Obtains values of the first row for the query.
- * @param values ContentValues that the values are stored to.
- * @param table Target table.
- * @param selection Query to select rows.
- * @param args Argument for query.
- */
- private void getFirstRow(String table, String selection, String[] args, ContentValues values) {
- values.clear();
- final Cursor cursor = mDatabase.query(table, null, selection, args, null, null, null, "1");
- if (cursor.getCount() == 0) {
- return;
- }
- cursor.moveToNext();
- DatabaseUtils.cursorRowToContentValues(cursor, values);
- cursor.close();
+ private String getRootDocumentsMappingStateKey(int deviceId) {
+ return "RootDocuments/" + deviceId;
}
- /**
- * Deletes a document, and its root information if the document is a root document.
- * @param selection Query to select documents.
- * @param args Arguments for selection.
- */
- private void deleteDocumentsAndRoots(String selection, String[] args) {
- mDatabase.beginTransaction();
- try {
- mDatabase.delete(
- TABLE_ROOT_EXTRA,
- Root.COLUMN_ROOT_ID + " IN (" + SQLiteQueryBuilder.buildQueryString(
- false,
- TABLE_DOCUMENTS,
- new String[] { Document.COLUMN_DOCUMENT_ID },
- selection,
- null,
- null,
- null,
- null) + ")",
- args);
- mDatabase.delete(TABLE_DOCUMENTS, selection, args);
- mDatabase.setTransactionSuccessful();
- } finally {
- mDatabase.endTransaction();
- }
- }
-
- /**
- * Converts values into string array.
- * @param args Values converted into string array.
- * @return String array.
- */
- private static String[] strings(Object... args) {
- final String[] results = new String[args.length];
- for (int i = 0; i < args.length; i++) {
- results[i] = Objects.toString(args[i]);
- }
- return results;
- }
-
- /**
- * Gets SQL expression that represents the given value or NULL depends on the row state.
- * @param state Expected row state.
- * @param a SQL value.
- * @return Expression that represents a if the row state is expected one, and represents NULL
- * otherwise.
- */
- private static String createStateFilter(int state, String a) {
- return "CASE WHEN " + COLUMN_ROW_STATE + " = " + Integer.toString(state) +
- " THEN " + a + " ELSE NULL END";
+ private String getChildDocumentsMappingStateKey(String parentDocumentId) {
+ return "ChildDocuments/" + parentDocumentId;
}
}
diff --git a/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDatabaseConstants.java b/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDatabaseConstants.java
new file mode 100644
index 0000000..5fb16ec
--- /dev/null
+++ b/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDatabaseConstants.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2015 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.mtp;
+
+import android.provider.DocumentsContract.Document;
+import android.provider.DocumentsContract.Root;
+
+/**
+ * Class containing MtpDatabase constants.
+ */
+class MtpDatabaseConstants {
+ static final int DATABASE_VERSION = 1;
+ static final String DATABASE_NAME = null;
+
+ /**
+ * Table representing documents including root documents.
+ */
+ static final String TABLE_DOCUMENTS = "Documents";
+
+ /**
+ * Table containing additional information only available for root documents.
+ * The table uses same primary keys with corresponding documents.
+ */
+ static final String TABLE_ROOT_EXTRA = "RootExtra";
+
+ /**
+ * View to join Documents and RootExtra tables to provide roots information.
+ */
+ static final String VIEW_ROOTS = "Roots";
+
+ static final String COLUMN_DEVICE_ID = "device_id";
+ static final String COLUMN_STORAGE_ID = "storage_id";
+ static final String COLUMN_OBJECT_HANDLE = "object_handle";
+ static final String COLUMN_PARENT_DOCUMENT_ID = "parent_document_id";
+ static final String COLUMN_ROW_STATE = "row_state";
+
+ /**
+ * The state represents that the row has a valid object handle.
+ */
+ static final int ROW_STATE_VALID = 0;
+
+ /**
+ * The state represents that the rows added at the previous cycle and need to be updated with
+ * fresh values.
+ * The row may not have valid object handle. External application can still fetch the documents.
+ * If the external application tries to fetch object handle, the provider resolves pending
+ * documents with invalidated documents ahead.
+ */
+ static final int ROW_STATE_INVALIDATED = 1;
+
+ /**
+ * The state represents the raw has a valid object handle but it may be going to be mapped with
+ * another rows invalidated. After fetching all documents under the parent, the database tries
+ * to map the pending documents and the invalidated documents in order to keep old document ID
+ * alive.
+ */
+ static final int ROW_STATE_PENDING = 2;
+
+ /**
+ * Mapping mode that uses MTP identifier to find corresponding rows.
+ */
+ static final int MAP_BY_MTP_IDENTIFIER = 0;
+
+ /**
+ * Mapping mode that uses name to find corresponding rows.
+ */
+ static final int MAP_BY_NAME = 1;
+
+ static final String SELECTION_DOCUMENT_ID = Document.COLUMN_DOCUMENT_ID + " = ?";
+ static final String SELECTION_ROOT_ID = Root.COLUMN_ROOT_ID + " = ?";
+ static final String SELECTION_ROOT_DOCUMENTS =
+ COLUMN_DEVICE_ID + " = ? AND " + COLUMN_PARENT_DOCUMENT_ID + " IS NULL";
+ static final String SELECTION_CHILD_DOCUMENTS = COLUMN_PARENT_DOCUMENT_ID + " = ?";
+
+ static final String QUERY_CREATE_DOCUMENTS =
+ "CREATE TABLE " + TABLE_DOCUMENTS + " (" +
+ Document.COLUMN_DOCUMENT_ID +
+ " INTEGER PRIMARY KEY AUTOINCREMENT," +
+ COLUMN_DEVICE_ID + " INTEGER NOT NULL," +
+ COLUMN_STORAGE_ID + " INTEGER," +
+ COLUMN_OBJECT_HANDLE + " INTEGER," +
+ COLUMN_PARENT_DOCUMENT_ID + " INTEGER," +
+ COLUMN_ROW_STATE + " INTEGER NOT NULL," +
+ Document.COLUMN_MIME_TYPE + " TEXT," +
+ Document.COLUMN_DISPLAY_NAME + " TEXT NOT NULL," +
+ Document.COLUMN_SUMMARY + " TEXT," +
+ Document.COLUMN_LAST_MODIFIED + " INTEGER," +
+ Document.COLUMN_ICON + " INTEGER," +
+ Document.COLUMN_FLAGS + " INTEGER NOT NULL," +
+ Document.COLUMN_SIZE + " INTEGER NOT NULL);";
+
+ static final String QUERY_CREATE_ROOT_EXTRA =
+ "CREATE TABLE " + TABLE_ROOT_EXTRA + " (" +
+ Root.COLUMN_ROOT_ID + " INTEGER PRIMARY KEY," +
+ Root.COLUMN_FLAGS + " INTEGER NOT NULL," +
+ Root.COLUMN_AVAILABLE_BYTES + " INTEGER NOT NULL," +
+ Root.COLUMN_CAPACITY_BYTES + " INTEGER NOT NULL," +
+ Root.COLUMN_MIME_TYPES + " TEXT NOT NULL);";
+
+ /**
+ * Creates a view to join Documents table and RootExtra table on their primary keys to
+ * provide DocumentContract.Root equivalent information.
+ */
+ static final String QUERY_CREATE_VIEW_ROOTS =
+ "CREATE VIEW " + VIEW_ROOTS + " AS SELECT " +
+ TABLE_DOCUMENTS + "." + Document.COLUMN_DOCUMENT_ID + " AS " +
+ Root.COLUMN_ROOT_ID + "," +
+ TABLE_ROOT_EXTRA + "." + Root.COLUMN_FLAGS + "," +
+ TABLE_DOCUMENTS + "." + Document.COLUMN_ICON + " AS " +
+ Root.COLUMN_ICON + "," +
+ TABLE_DOCUMENTS + "." + Document.COLUMN_DISPLAY_NAME + " AS " +
+ Root.COLUMN_TITLE + "," +
+ TABLE_DOCUMENTS + "." + Document.COLUMN_SUMMARY + " AS " +
+ Root.COLUMN_SUMMARY + "," +
+ TABLE_DOCUMENTS + "." + Document.COLUMN_DOCUMENT_ID + " AS " +
+ Root.COLUMN_DOCUMENT_ID + "," +
+ TABLE_ROOT_EXTRA + "." + Root.COLUMN_AVAILABLE_BYTES + "," +
+ TABLE_ROOT_EXTRA + "." + Root.COLUMN_CAPACITY_BYTES + "," +
+ TABLE_ROOT_EXTRA + "." + Root.COLUMN_MIME_TYPES + "," +
+ TABLE_DOCUMENTS + "." + COLUMN_ROW_STATE +
+ " FROM " + TABLE_DOCUMENTS + " INNER JOIN " + TABLE_ROOT_EXTRA +
+ " ON " +
+ COLUMN_PARENT_DOCUMENT_ID + " IS NULL AND " +
+ TABLE_DOCUMENTS + "." + Document.COLUMN_DOCUMENT_ID +
+ "=" +
+ TABLE_ROOT_EXTRA + "." + Root.COLUMN_ROOT_ID;
+}
diff --git a/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDatabaseInternal.java b/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDatabaseInternal.java
new file mode 100644
index 0000000..730012d
--- /dev/null
+++ b/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDatabaseInternal.java
@@ -0,0 +1,391 @@
+/*
+ * Copyright (C) 2015 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.mtp;
+
+import static com.android.mtp.MtpDatabaseConstants.*;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.provider.DocumentsContract.Document;
+import android.provider.DocumentsContract.Root;
+
+import java.util.Objects;
+
+/**
+ * Class that provides operations processing SQLite database directly.
+ */
+class MtpDatabaseInternal {
+ private static class OpenHelper extends SQLiteOpenHelper {
+ public OpenHelper(Context context) {
+ super(context, DATABASE_NAME, null, DATABASE_VERSION);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ db.execSQL(QUERY_CREATE_DOCUMENTS);
+ db.execSQL(QUERY_CREATE_ROOT_EXTRA);
+ db.execSQL(QUERY_CREATE_VIEW_ROOTS);
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ private final SQLiteDatabase mDatabase;
+
+ MtpDatabaseInternal(Context context) {
+ final OpenHelper helper = new OpenHelper(context);
+ mDatabase = helper.getWritableDatabase();
+ }
+
+ Cursor queryRoots(String[] columnNames) {
+ return mDatabase.query(
+ VIEW_ROOTS,
+ columnNames,
+ COLUMN_ROW_STATE + " IN (?, ?)",
+ strings(ROW_STATE_VALID, ROW_STATE_INVALIDATED),
+ null,
+ null,
+ null);
+ }
+
+ Cursor queryRootDocuments(String[] columnNames) {
+ return mDatabase.query(
+ TABLE_DOCUMENTS,
+ columnNames,
+ COLUMN_ROW_STATE + " IN (?, ?)",
+ strings(ROW_STATE_VALID, ROW_STATE_INVALIDATED),
+ null,
+ null,
+ null);
+ }
+
+ Cursor queryChildDocuments(String[] columnNames, String parentDocumentId) {
+ return mDatabase.query(
+ TABLE_DOCUMENTS,
+ columnNames,
+ COLUMN_ROW_STATE + " IN (?, ?) AND " + COLUMN_PARENT_DOCUMENT_ID + " = ?",
+ strings(ROW_STATE_VALID, ROW_STATE_INVALIDATED, parentDocumentId),
+ null,
+ null,
+ null);
+ }
+
+ /**
+ * Starts adding new documents.
+ * The methods decides mapping mode depends on if all documents under the given parent have MTP
+ * identifier or not. If all the documents have MTP identifier, it uses the identifier to find
+ * a corresponding existing row. Otherwise it does heuristic.
+ *
+ * @param selection Query matches valid documents.
+ * @param arg Argument for selection.
+ * @return Mapping mode.
+ */
+ int startAddingDocuments(String selection, String arg) {
+ mDatabase.beginTransaction();
+ try {
+ // Delete all pending rows.
+ deleteDocumentsAndRoots(
+ selection + " AND " + COLUMN_ROW_STATE + "=?", strings(arg, ROW_STATE_PENDING));
+
+ // Set all documents as invalidated.
+ final ContentValues values = new ContentValues();
+ values.put(COLUMN_ROW_STATE, ROW_STATE_INVALIDATED);
+ mDatabase.update(TABLE_DOCUMENTS, values, selection, new String[] { arg });
+
+ // If we have rows that does not have MTP identifier, do heuristic mapping by name.
+ final boolean useNameForResolving = DatabaseUtils.queryNumEntries(
+ mDatabase,
+ TABLE_DOCUMENTS,
+ selection + " AND " + COLUMN_STORAGE_ID + " IS NULL",
+ new String[] { arg }) > 0;
+ mDatabase.setTransactionSuccessful();
+ return useNameForResolving ? MAP_BY_NAME : MAP_BY_MTP_IDENTIFIER;
+ } finally {
+ mDatabase.endTransaction();
+ }
+ }
+
+ /**
+ * Puts the documents into the database.
+ * If the mapping mode is not heuristic, it just adds the rows to the database or updates the
+ * existing rows with the new values. If the mapping mode is heuristic, it adds some new rows as
+ * 'pending' state when that rows may be corresponding to existing 'invalidated' rows. Then
+ * {@link #stopAddingDocuments(String, String, String)} turns the pending rows into 'valid'
+ * rows.
+ *
+ * @param valuesList Values that are stored in the database.
+ * @param selection SQL where closure to select rows that shares the same parent.
+ * @param arg Argument for selection SQL.
+ * @param heuristic Whether the mapping mode is heuristic.
+ * @return List of Document ID inserted to the table.
+ */
+ long[] putDocuments(
+ ContentValues[] valuesList,
+ String selection,
+ String arg,
+ boolean heuristic,
+ String mappingKey) {
+ mDatabase.beginTransaction();
+ try {
+ final long[] documentIds = new long[valuesList.length];
+ int i = 0;
+ for (final ContentValues values : valuesList) {
+ final Cursor candidateCursor = mDatabase.query(
+ TABLE_DOCUMENTS,
+ strings(Document.COLUMN_DOCUMENT_ID),
+ selection + " AND " +
+ COLUMN_ROW_STATE + "=? AND " +
+ mappingKey + "=?",
+ strings(arg, ROW_STATE_INVALIDATED, values.getAsString(mappingKey)),
+ null,
+ null,
+ null,
+ "1");
+ final long rowId;
+ if (candidateCursor.getCount() == 0) {
+ rowId = mDatabase.insert(TABLE_DOCUMENTS, null, values);
+ } else if (!heuristic) {
+ candidateCursor.moveToNext();
+ final String documentId = candidateCursor.getString(0);
+ rowId = mDatabase.update(
+ TABLE_DOCUMENTS, values, SELECTION_DOCUMENT_ID, strings(documentId));
+ } else {
+ values.put(COLUMN_ROW_STATE, ROW_STATE_PENDING);
+ rowId = mDatabase.insert(TABLE_DOCUMENTS, null, values);
+ }
+ // Document ID is a primary integer key of the table. So the returned row
+ // IDs should be same with the document ID.
+ documentIds[i++] = rowId;
+ candidateCursor.close();
+ }
+
+ mDatabase.setTransactionSuccessful();
+ return documentIds;
+ } finally {
+ mDatabase.endTransaction();
+ }
+ }
+
+ void putRootExtra(ContentValues values) {
+ mDatabase.replace(TABLE_ROOT_EXTRA, null, values);
+ }
+
+ /**
+ * Maps 'pending' document and 'invalidated' document that shares the same column of groupKey.
+ * If the database does not find corresponding 'invalidated' document, it just removes
+ * 'invalidated' document from the database.
+ * @param selection Query to select rows for resolving.
+ * @param arg Argument for selection SQL.
+ * @param groupKey Column name used to find corresponding rows.
+ */
+ void stopAddingDocuments(String selection, String arg, String groupKey) {
+ mDatabase.beginTransaction();
+ try {
+ // Get 1-to-1 mapping of invalidated document and pending document.
+ final String invalidatedIdQuery = createStateFilter(
+ ROW_STATE_INVALIDATED, Document.COLUMN_DOCUMENT_ID);
+ final String pendingIdQuery = createStateFilter(
+ ROW_STATE_PENDING, Document.COLUMN_DOCUMENT_ID);
+ // SQL should be like:
+ // SELECT group_concat(CASE WHEN raw_state = 1 THEN document_id ELSE NULL END),
+ // group_concat(CASE WHEN raw_state = 2 THEN document_id ELSE NULL END)
+ // WHERE device_id = ? AND parent_document_id IS NULL
+ // GROUP BY display_name
+ // HAVING count(CASE WHEN raw_state = 1 THEN document_id ELSE NULL END) = 1 AND
+ // count(CASE WHEN raw_state = 2 THEN document_id ELSE NULL END) = 1
+ final Cursor mergingCursor = mDatabase.query(
+ TABLE_DOCUMENTS,
+ new String[] {
+ "group_concat(" + invalidatedIdQuery + ")",
+ "group_concat(" + pendingIdQuery + ")"
+ },
+ selection,
+ strings(arg),
+ groupKey,
+ "count(" + invalidatedIdQuery + ") = 1 AND count(" + pendingIdQuery + ") = 1",
+ null);
+
+ final ContentValues values = new ContentValues();
+ while (mergingCursor.moveToNext()) {
+ final String invalidatedId = mergingCursor.getString(0);
+ final String pendingId = mergingCursor.getString(1);
+
+ // Obtain the new values including the latest object handle from mapping row.
+ getFirstRow(
+ TABLE_DOCUMENTS,
+ SELECTION_DOCUMENT_ID,
+ new String[] { pendingId },
+ values);
+ values.remove(Document.COLUMN_DOCUMENT_ID);
+ values.put(COLUMN_ROW_STATE, ROW_STATE_VALID);
+ mDatabase.update(
+ TABLE_DOCUMENTS,
+ values,
+ SELECTION_DOCUMENT_ID,
+ new String[] { invalidatedId });
+
+ getFirstRow(
+ TABLE_ROOT_EXTRA,
+ SELECTION_ROOT_ID,
+ new String[] { pendingId },
+ values);
+ if (values.size() > 0) {
+ values.remove(Root.COLUMN_ROOT_ID);
+ mDatabase.update(
+ TABLE_ROOT_EXTRA,
+ values,
+ SELECTION_ROOT_ID,
+ new String[] { invalidatedId });
+ }
+
+ // Delete 'pending' row.
+ deleteDocumentsAndRoots(SELECTION_DOCUMENT_ID, new String[] { pendingId });
+ }
+ mergingCursor.close();
+
+ // Delete all invalidated rows that cannot be mapped.
+ deleteDocumentsAndRoots(
+ COLUMN_ROW_STATE + " = ? AND " + selection,
+ strings(ROW_STATE_INVALIDATED, arg));
+
+ // The database cannot find old document ID for the pending rows.
+ // Turn the all pending rows into valid state, which means the rows become to be
+ // valid with new document ID.
+ values.clear();
+ values.put(COLUMN_ROW_STATE, ROW_STATE_VALID);
+ mDatabase.update(
+ TABLE_DOCUMENTS,
+ values,
+ COLUMN_ROW_STATE + " = ? AND " + selection,
+ strings(ROW_STATE_PENDING, arg));
+ mDatabase.setTransactionSuccessful();
+ } finally {
+ mDatabase.endTransaction();
+ }
+ }
+
+ /**
+ * Clears MTP related identifier.
+ * It clears MTP's object handle and storage ID that are not stable over MTP sessions and mark
+ * the all documents as 'invalidated'. It also remove 'pending' rows as adding is cancelled
+ * now.
+ */
+ void clearMapping() {
+ mDatabase.beginTransaction();
+ try {
+ deleteDocumentsAndRoots(COLUMN_ROW_STATE + " = ?", strings(ROW_STATE_PENDING));
+ final ContentValues values = new ContentValues();
+ values.putNull(COLUMN_OBJECT_HANDLE);
+ values.putNull(COLUMN_STORAGE_ID);
+ values.put(COLUMN_ROW_STATE, ROW_STATE_INVALIDATED);
+ mDatabase.update(TABLE_DOCUMENTS, values, null, null);
+ mDatabase.setTransactionSuccessful();
+ } finally {
+ mDatabase.endTransaction();
+ }
+ }
+
+ void beginTransaction() {
+ mDatabase.beginTransaction();
+ }
+
+ void setTransactionSuccessful() {
+ mDatabase.setTransactionSuccessful();
+ }
+
+ void endTransaction() {
+ mDatabase.endTransaction();
+ }
+
+ /**
+ * Deletes a document, and its root information if the document is a root document.
+ * @param selection Query to select documents.
+ * @param args Arguments for selection.
+ */
+ private void deleteDocumentsAndRoots(String selection, String[] args) {
+ mDatabase.beginTransaction();
+ try {
+ mDatabase.delete(
+ TABLE_ROOT_EXTRA,
+ Root.COLUMN_ROOT_ID + " IN (" + SQLiteQueryBuilder.buildQueryString(
+ false,
+ TABLE_DOCUMENTS,
+ new String[] { Document.COLUMN_DOCUMENT_ID },
+ selection,
+ null,
+ null,
+ null,
+ null) + ")",
+ args);
+ mDatabase.delete(TABLE_DOCUMENTS, selection, args);
+ mDatabase.setTransactionSuccessful();
+ } finally {
+ mDatabase.endTransaction();
+ }
+ }
+
+ /**
+ * Obtains values of the first row for the query.
+ * @param values ContentValues that the values are stored to.
+ * @param table Target table.
+ * @param selection Query to select rows.
+ * @param args Argument for query.
+ */
+ private void getFirstRow(String table, String selection, String[] args, ContentValues values) {
+ values.clear();
+ final Cursor cursor = mDatabase.query(table, null, selection, args, null, null, null, "1");
+ if (cursor.getCount() == 0) {
+ return;
+ }
+ cursor.moveToNext();
+ DatabaseUtils.cursorRowToContentValues(cursor, values);
+ cursor.close();
+ }
+
+ /**
+ * Gets SQL expression that represents the given value or NULL depends on the row state.
+ * @param state Expected row state.
+ * @param a SQL value.
+ * @return Expression that represents a if the row state is expected one, and represents NULL
+ * otherwise.
+ */
+ private static String createStateFilter(int state, String a) {
+ return "CASE WHEN " + COLUMN_ROW_STATE + " = " + Integer.toString(state) +
+ " THEN " + a + " ELSE NULL END";
+ }
+
+ /**
+ * Converts values into string array.
+ * @param args Values converted into string array.
+ * @return String array.
+ */
+ private static String[] strings(Object... args) {
+ final String[] results = new String[args.length];
+ for (int i = 0; i < args.length; i++) {
+ results[i] = Objects.toString(args[i]);
+ }
+ return results;
+ }
+}
diff --git a/packages/MtpDocumentsProvider/tests/src/com/android/mtp/MtpDatabaseTest.java b/packages/MtpDocumentsProvider/tests/src/com/android/mtp/MtpDatabaseTest.java
index 3878ba6..05345e1 100644
--- a/packages/MtpDocumentsProvider/tests/src/com/android/mtp/MtpDatabaseTest.java
+++ b/packages/MtpDocumentsProvider/tests/src/com/android/mtp/MtpDatabaseTest.java
@@ -28,9 +28,9 @@
public class MtpDatabaseTest extends AndroidTestCase {
private final String[] COLUMN_NAMES = new String[] {
DocumentsContract.Document.COLUMN_DOCUMENT_ID,
- MtpDatabase.COLUMN_DEVICE_ID,
- MtpDatabase.COLUMN_STORAGE_ID,
- MtpDatabase.COLUMN_OBJECT_HANDLE,
+ MtpDatabaseConstants.COLUMN_DEVICE_ID,
+ MtpDatabaseConstants.COLUMN_STORAGE_ID,
+ MtpDatabaseConstants.COLUMN_OBJECT_HANDLE,
DocumentsContract.Document.COLUMN_MIME_TYPE,
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
DocumentsContract.Document.COLUMN_SUMMARY,
@@ -42,13 +42,9 @@
private final TestResources resources = new TestResources();
- @Override
- public void tearDown() {
- MtpDatabase.deleteDatabase(getContext());
- }
-
public void testPutRootDocuments() throws Exception {
final MtpDatabase database = new MtpDatabase(getContext());
+ database.startAddingRootDocuments(0);
database.putRootDocuments(0, resources, new MtpRoot[] {
new MtpRoot(0, 1, "Device", "Storage", 1000, 2000, ""),
new MtpRoot(0, 2, "Device", "Storage", 2000, 4000, ""),
@@ -141,7 +137,7 @@
public void testPutChildDocuments() throws Exception {
final MtpDatabase database = new MtpDatabase(getContext());
-
+ database.startAddingChildDocuments("parentId");
database.putChildDocuments(0, "parentId", new MtpObjectInfo[] {
createDocument(100, "note.txt", MtpConstants.FORMAT_TEXT, 1024),
createDocument(101, "image.jpg", MtpConstants.FORMAT_EXIF_JPEG, 2 * 1024 * 1024),
@@ -209,13 +205,14 @@
final MtpDatabase database = new MtpDatabase(getContext());
final String[] columns = new String[] {
DocumentsContract.Document.COLUMN_DOCUMENT_ID,
- MtpDatabase.COLUMN_STORAGE_ID,
+ MtpDatabaseConstants.COLUMN_STORAGE_ID,
DocumentsContract.Document.COLUMN_DISPLAY_NAME
};
final String[] rootColumns = new String[] {
Root.COLUMN_ROOT_ID,
Root.COLUMN_AVAILABLE_BYTES
};
+ database.startAddingRootDocuments(0);
database.putRootDocuments(0, resources, new MtpRoot[] {
new MtpRoot(0, 100, "Device", "Storage A", 1000, 0, ""),
new MtpRoot(0, 101, "Device", "Storage B", 1001, 0, "")
@@ -275,6 +272,7 @@
cursor.close();
}
+ database.startAddingRootDocuments(0);
database.putRootDocuments(0, resources, new MtpRoot[] {
new MtpRoot(0, 200, "Device", "Storage A", 2000, 0, ""),
new MtpRoot(0, 202, "Device", "Storage C", 2002, 0, "")
@@ -313,7 +311,7 @@
cursor.close();
}
- database.resolveRootDocuments(0);
+ database.stopAddingRootDocuments(0);
{
final Cursor cursor = database.queryRootDocuments(columns);
@@ -346,9 +344,10 @@
final MtpDatabase database = new MtpDatabase(getContext());
final String[] columns = new String[] {
DocumentsContract.Document.COLUMN_DOCUMENT_ID,
- MtpDatabase.COLUMN_OBJECT_HANDLE,
+ MtpDatabaseConstants.COLUMN_OBJECT_HANDLE,
DocumentsContract.Document.COLUMN_DISPLAY_NAME
};
+ database.startAddingChildDocuments("parentId");
database.putChildDocuments(0, "parentId", new MtpObjectInfo[] {
createDocument(100, "note.txt", MtpConstants.FORMAT_TEXT, 1024),
createDocument(101, "image.jpg", MtpConstants.FORMAT_EXIF_JPEG, 2 * 1024 * 1024),
@@ -378,6 +377,7 @@
cursor.close();
}
+ database.startAddingChildDocuments("parentId");
database.putChildDocuments(0, "parentId", new MtpObjectInfo[] {
createDocument(200, "note.txt", MtpConstants.FORMAT_TEXT, 1024),
createDocument(203, "video.mp4", MtpConstants.FORMAT_MP4_CONTAINER, 1024),
@@ -395,7 +395,7 @@
cursor.close();
}
- database.resolveChildDocuments("parentId");
+ database.stopAddingChildDocuments("parentId");
{
final Cursor cursor = database.queryChildDocuments(columns, "parentId");
@@ -418,13 +418,15 @@
final MtpDatabase database = new MtpDatabase(getContext());
final String[] columns = new String[] {
DocumentsContract.Document.COLUMN_DOCUMENT_ID,
- MtpDatabase.COLUMN_STORAGE_ID,
+ MtpDatabaseConstants.COLUMN_STORAGE_ID,
DocumentsContract.Document.COLUMN_DISPLAY_NAME
};
final String[] rootColumns = new String[] {
Root.COLUMN_ROOT_ID,
Root.COLUMN_AVAILABLE_BYTES
};
+ database.startAddingRootDocuments(0);
+ database.startAddingRootDocuments(1);
database.putRootDocuments(0, resources, new MtpRoot[] {
new MtpRoot(0, 100, "Device", "Storage", 0, 0, "")
});
@@ -460,14 +462,16 @@
database.clearMapping();
+ database.startAddingRootDocuments(0);
+ database.startAddingRootDocuments(1);
database.putRootDocuments(0, resources, new MtpRoot[] {
new MtpRoot(0, 200, "Device", "Storage", 2000, 0, "")
});
database.putRootDocuments(1, resources, new MtpRoot[] {
new MtpRoot(1, 300, "Device", "Storage", 3000, 0, "")
});
- database.resolveRootDocuments(0);
- database.resolveRootDocuments(1);
+ database.stopAddingRootDocuments(0);
+ database.stopAddingRootDocuments(1);
{
final Cursor cursor = database.queryRootDocuments(columns);
@@ -500,8 +504,11 @@
final MtpDatabase database = new MtpDatabase(getContext());
final String[] columns = new String[] {
DocumentsContract.Document.COLUMN_DOCUMENT_ID,
- MtpDatabase.COLUMN_OBJECT_HANDLE
+ MtpDatabaseConstants.COLUMN_OBJECT_HANDLE
};
+
+ database.startAddingChildDocuments("parentId1");
+ database.startAddingChildDocuments("parentId2");
database.putChildDocuments(0, "parentId1", new MtpObjectInfo[] {
createDocument(100, "note.txt", MtpConstants.FORMAT_TEXT, 1024),
});
@@ -509,13 +516,16 @@
createDocument(101, "note.txt", MtpConstants.FORMAT_TEXT, 1024),
});
database.clearMapping();
+
+ database.startAddingChildDocuments("parentId1");
+ database.startAddingChildDocuments("parentId2");
database.putChildDocuments(0, "parentId1", new MtpObjectInfo[] {
createDocument(200, "note.txt", MtpConstants.FORMAT_TEXT, 1024),
});
database.putChildDocuments(0, "parentId2", new MtpObjectInfo[] {
createDocument(201, "note.txt", MtpConstants.FORMAT_TEXT, 1024),
});
- database.resolveChildDocuments("parentId1");
+ database.stopAddingChildDocuments("parentId1");
{
final Cursor cursor = database.queryChildDocuments(columns, "parentId1");
@@ -539,25 +549,32 @@
final MtpDatabase database = new MtpDatabase(getContext());
final String[] columns = new String[] {
DocumentsContract.Document.COLUMN_DOCUMENT_ID,
- MtpDatabase.COLUMN_STORAGE_ID,
+ MtpDatabaseConstants.COLUMN_STORAGE_ID,
DocumentsContract.Document.COLUMN_DISPLAY_NAME
};
final String[] rootColumns = new String[] {
Root.COLUMN_ROOT_ID,
Root.COLUMN_AVAILABLE_BYTES
};
+
+ database.startAddingRootDocuments(0);
database.putRootDocuments(0, resources, new MtpRoot[] {
new MtpRoot(0, 100, "Device", "Storage", 0, 0, ""),
});
database.clearMapping();
+
+ database.startAddingRootDocuments(0);
database.putRootDocuments(0, resources, new MtpRoot[] {
new MtpRoot(0, 200, "Device", "Storage", 2000, 0, ""),
});
database.clearMapping();
+
+ database.startAddingRootDocuments(0);
database.putRootDocuments(0, resources, new MtpRoot[] {
new MtpRoot(0, 300, "Device", "Storage", 3000, 0, ""),
});
- database.resolveRootDocuments(0);
+ database.stopAddingRootDocuments(0);
+
{
final Cursor cursor = database.queryRootDocuments(columns);
assertEquals(1, cursor.getCount());
@@ -581,22 +598,27 @@
final MtpDatabase database = new MtpDatabase(getContext());
final String[] columns = new String[] {
DocumentsContract.Document.COLUMN_DOCUMENT_ID,
- MtpDatabase.COLUMN_STORAGE_ID,
+ MtpDatabaseConstants.COLUMN_STORAGE_ID,
DocumentsContract.Document.COLUMN_DISPLAY_NAME
};
final String[] rootColumns = new String[] {
Root.COLUMN_ROOT_ID,
Root.COLUMN_AVAILABLE_BYTES
};
+
+ database.startAddingRootDocuments(0);
database.putRootDocuments(0, resources, new MtpRoot[] {
new MtpRoot(0, 100, "Device", "Storage", 0, 0, ""),
});
database.clearMapping();
+
+ database.startAddingRootDocuments(0);
database.putRootDocuments(0, resources, new MtpRoot[] {
new MtpRoot(0, 200, "Device", "Storage", 2000, 0, ""),
new MtpRoot(0, 201, "Device", "Storage", 2001, 0, ""),
});
- database.resolveRootDocuments(0);
+ database.stopAddingRootDocuments(0);
+
{
final Cursor cursor = database.queryRootDocuments(columns);
assertEquals(2, cursor.getCount());
@@ -622,4 +644,71 @@
cursor.close();
}
}
+
+ public void testReplaceExistingRoots() {
+ // The client code should be able to replace exisitng rows with new information.
+ final MtpDatabase database = new MtpDatabase(getContext());
+ // Add one.
+ database.startAddingRootDocuments(0);
+ database.putRootDocuments(0, resources, new MtpRoot[] {
+ new MtpRoot(0, 100, "Device", "Storage A", 0, 0, ""),
+ });
+ database.stopAddingRootDocuments(0);
+ // Replace it.
+ database.startAddingRootDocuments(0);
+ database.putRootDocuments(0, resources, new MtpRoot[] {
+ new MtpRoot(0, 100, "Device", "Storage B", 1000, 1000, ""),
+ });
+ database.stopAddingRootDocuments(0);
+ {
+ final String[] columns = new String[] {
+ DocumentsContract.Document.COLUMN_DOCUMENT_ID,
+ MtpDatabaseConstants.COLUMN_STORAGE_ID,
+ DocumentsContract.Document.COLUMN_DISPLAY_NAME
+ };
+ final Cursor cursor = database.queryRootDocuments(columns);
+ assertEquals(1, cursor.getCount());
+ cursor.moveToNext();
+ assertEquals("documentId", 1, cursor.getInt(0));
+ assertEquals("storageId", 100, cursor.getInt(1));
+ assertEquals("name", "Device Storage B", cursor.getString(2));
+ cursor.close();
+ }
+ {
+ final String[] columns = new String[] {
+ Root.COLUMN_ROOT_ID,
+ Root.COLUMN_AVAILABLE_BYTES
+ };
+ final Cursor cursor = database.queryRoots(columns);
+ assertEquals(1, cursor.getCount());
+ cursor.moveToNext();
+ assertEquals("rootId", 1, cursor.getInt(0));
+ assertEquals("availableBytes", 1000, cursor.getInt(1));
+ cursor.close();
+ }
+ }
+
+ public void _testFailToReplaceExisitingUnmappedRoots() {
+ // The client code should not be able to replace rows before resolving 'unmapped' rows.
+ final MtpDatabase database = new MtpDatabase(getContext());
+ // Add one.
+ database.startAddingRootDocuments(0);
+ database.putRootDocuments(0, resources, new MtpRoot[] {
+ new MtpRoot(0, 100, "Device", "Storage A", 0, 0, ""),
+ });
+ database.clearMapping();
+ // Add one.
+ database.putRootDocuments(0, resources, new MtpRoot[] {
+ new MtpRoot(0, 100, "Device", "Storage B", 1000, 1000, ""),
+ });
+ // Add one more before resolving unmapped documents.
+ try {
+ database.putRootDocuments(0, resources, new MtpRoot[] {
+ new MtpRoot(0, 100, "Device", "Storage B", 1000, 1000, ""),
+ });
+ fail();
+ } catch (Throwable e) {
+ assertTrue(e instanceof Error);
+ }
+ }
}
diff --git a/packages/PrintSpooler/res/values/strings.xml b/packages/PrintSpooler/res/values/strings.xml
index 50237832..70abdf4 100644
--- a/packages/PrintSpooler/res/values/strings.xml
+++ b/packages/PrintSpooler/res/values/strings.xml
@@ -149,6 +149,9 @@
<!-- Title for the prompt shown as a placeholder if no printers are found while not searching. [CHAR LIMIT=50] -->
<string name="print_searching_for_printers">Searching for printers</string>
+ <!-- Title for the prompt shown as a placeholder if there are no print services. [CHAR LIMIT=50] -->
+ <string name="print_no_print_services">No print services enabled</string>
+
<!-- Title for the prompt shown as a placeholder if there are no printers while searching. [CHAR LIMIT=50] -->
<string name="print_no_printers">No printers found</string>
diff --git a/packages/PrintSpooler/src/com/android/printspooler/ui/SelectPrinterActivity.java b/packages/PrintSpooler/src/com/android/printspooler/ui/SelectPrinterActivity.java
index 3905bada..f4c15bd 100644
--- a/packages/PrintSpooler/src/com/android/printspooler/ui/SelectPrinterActivity.java
+++ b/packages/PrintSpooler/src/com/android/printspooler/ui/SelectPrinterActivity.java
@@ -84,6 +84,11 @@
private static final String EXTRA_PRINTER_ID = "EXTRA_PRINTER_ID";
+ /**
+ * If there are any enabled print services
+ */
+ private boolean mHasEnabledPrintServices;
+
private final ArrayList<PrintServiceInfo> mAddPrinterServices =
new ArrayList<>();
@@ -175,10 +180,6 @@
}
});
- if (mAddPrinterServices.isEmpty()) {
- menu.removeItem(R.id.action_add_printer);
- }
-
return true;
}
@@ -230,6 +231,7 @@
public void onResume() {
super.onResume();
updateServicesWithAddPrinterActivity();
+ updateEmptyView((DestinationAdapter)mListView.getAdapter());
invalidateOptionsMenu();
}
@@ -258,6 +260,7 @@
}
private void updateServicesWithAddPrinterActivity() {
+ mHasEnabledPrintServices = true;
mAddPrinterServices.clear();
// Get all enabled print services.
@@ -266,6 +269,7 @@
// No enabled print services - done.
if (enabledServices.isEmpty()) {
+ mHasEnabledPrintServices = false;
return;
}
@@ -324,7 +328,10 @@
}
TextView titleView = (TextView) findViewById(R.id.title);
View progressBar = findViewById(R.id.progress_bar);
- if (adapter.getUnfilteredCount() <= 0) {
+ if (!mHasEnabledPrintServices) {
+ titleView.setText(R.string.print_no_print_services);
+ progressBar.setVisibility(View.GONE);
+ } else if (adapter.getUnfilteredCount() <= 0) {
titleView.setText(R.string.print_searching_for_printers);
progressBar.setVisibility(View.VISIBLE);
} else {
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
index e4b1ed8..d994841 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
@@ -808,7 +808,12 @@
// The pairing dialog now warns of phone-book access for paired devices.
// No separate prompt is displayed after pairing.
if (getPhonebookPermissionChoice() == CachedBluetoothDevice.ACCESS_UNKNOWN) {
- setPhonebookPermissionChoice(CachedBluetoothDevice.ACCESS_ALLOWED);
+ if (mDevice.getBluetoothClass().getDeviceClass()
+ == BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE) {
+ setPhonebookPermissionChoice(CachedBluetoothDevice.ACCESS_ALLOWED);
+ } else {
+ setPhonebookPermissionChoice(CachedBluetoothDevice.ACCESS_REJECTED);
+ }
}
}
}
diff --git a/packages/SystemUI/res/anim/recents_fast_toggle_app_home_exit.xml b/packages/SystemUI/res/anim/recents_fast_toggle_app_home_exit.xml
new file mode 100644
index 0000000..69edcc7
--- /dev/null
+++ b/packages/SystemUI/res/anim/recents_fast_toggle_app_home_exit.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2012, 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.
+*/
+-->
+<!-- Recents Activity -->
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shareInterpolator="false"
+ android:zAdjustment="top">
+ <alpha android:fromAlpha="1.0" android:toAlpha="1.0"
+ android:fillEnabled="true"
+ android:fillBefore="true" android:fillAfter="true"
+ android:duration="250"/>
+</set>
diff --git a/packages/SystemUI/src/com/android/systemui/recents/Recents.java b/packages/SystemUI/src/com/android/systemui/recents/Recents.java
index bf5417d..95f1eb2 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/Recents.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/Recents.java
@@ -27,6 +27,7 @@
import android.os.RemoteException;
import android.os.SystemProperties;
import android.os.UserHandle;
+import android.provider.Settings;
import android.util.Log;
import android.view.Display;
import android.view.View;
@@ -193,6 +194,12 @@
*/
@Override
public void showRecents(boolean triggeredFromAltTab, View statusBarView) {
+ // Ensure the device has been provisioned before allowing the user to interact with
+ // recents
+ if (!isDeviceProvisioned()) {
+ return;
+ }
+
if (proxyToOverridePackage(ACTION_SHOW_RECENTS)) {
return;
}
@@ -222,6 +229,12 @@
*/
@Override
public void hideRecents(boolean triggeredFromAltTab, boolean triggeredFromHomeKey) {
+ // Ensure the device has been provisioned before allowing the user to interact with
+ // recents
+ if (!isDeviceProvisioned()) {
+ return;
+ }
+
if (proxyToOverridePackage(ACTION_HIDE_RECENTS)) {
return;
}
@@ -251,6 +264,12 @@
*/
@Override
public void toggleRecents(Display display, int layoutDirection, View statusBarView) {
+ // Ensure the device has been provisioned before allowing the user to interact with
+ // recents
+ if (!isDeviceProvisioned()) {
+ return;
+ }
+
if (proxyToOverridePackage(ACTION_TOGGLE_RECENTS)) {
return;
}
@@ -280,6 +299,12 @@
*/
@Override
public void preloadRecents() {
+ // Ensure the device has been provisioned before allowing the user to interact with
+ // recents
+ if (!isDeviceProvisioned()) {
+ return;
+ }
+
int currentUser = sSystemServicesProxy.getCurrentUser();
if (sSystemServicesProxy.isSystemUser(currentUser)) {
mImpl.preloadRecents();
@@ -302,6 +327,12 @@
@Override
public void cancelPreloadingRecents() {
+ // Ensure the device has been provisioned before allowing the user to interact with
+ // recents
+ if (!isDeviceProvisioned()) {
+ return;
+ }
+
int currentUser = sSystemServicesProxy.getCurrentUser();
if (sSystemServicesProxy.isSystemUser(currentUser)) {
mImpl.cancelPreloadingRecents();
@@ -329,11 +360,23 @@
@Override
public void showNextAffiliatedTask() {
+ // Ensure the device has been provisioned before allowing the user to interact with
+ // recents
+ if (!isDeviceProvisioned()) {
+ return;
+ }
+
mImpl.showNextAffiliatedTask();
}
@Override
public void showPrevAffiliatedTask() {
+ // Ensure the device has been provisioned before allowing the user to interact with
+ // recents
+ if (!isDeviceProvisioned()) {
+ return;
+ }
+
mImpl.showPrevAffiliatedTask();
}
@@ -456,6 +499,14 @@
}
/**
+ * @return whether this device is provisioned.
+ */
+ private boolean isDeviceProvisioned() {
+ return Settings.Global.getInt(mContext.getContentResolver(),
+ Settings.Global.DEVICE_PROVISIONED, 0) != 0;
+ }
+
+ /**
* Attempts to proxy the following action to the override recents package.
* @return whether the proxying was successful
*/
diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java
index 6fe6909..50aa2f7 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java
@@ -401,17 +401,22 @@
ActivityManager.RunningTaskInfo runningTask = ssp.getTopMostTask();
// Return early if there is no running task
if (runningTask == null) return;
- // Return early if the running task is in the home stack (optimization)
- if (SystemServicesProxy.isHomeStack(runningTask.stackId)) return;
// Find the task in the recents list
+ boolean isTopTaskHome = SystemServicesProxy.isHomeStack(runningTask.stackId);
ArrayList<Task> tasks = focusedStack.getTasks();
Task toTask = null;
ActivityOptions launchOpts = null;
int taskCount = tasks.size();
for (int i = taskCount - 1; i >= 1; i--) {
Task task = tasks.get(i);
- if (task.key.id == runningTask.id) {
+ if (isTopTaskHome) {
+ toTask = tasks.get(i - 1);
+ launchOpts = ActivityOptions.makeCustomAnimation(mContext,
+ R.anim.recents_launch_next_affiliated_task_target,
+ R.anim.recents_fast_toggle_app_home_exit);
+ break;
+ } else if (task.key.id == runningTask.id) {
toTask = tasks.get(i - 1);
launchOpts = ActivityOptions.makeCustomAnimation(mContext,
R.anim.recents_launch_prev_affiliated_task_target,
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/RecentsTransitionHelper.java b/packages/SystemUI/src/com/android/systemui/recents/views/RecentsTransitionHelper.java
index a28601b..85b8fcf 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/RecentsTransitionHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/RecentsTransitionHelper.java
@@ -270,7 +270,7 @@
int taskCount = tasks.size();
for (int i = taskCount - 1; i >= 0; i--) {
Task t = tasks.get(i);
- if (t.isFreeformTask()) {
+ if (t.isFreeformTask() || targetStackId == FREEFORM_WORKSPACE_STACK_ID) {
TaskView tv = stackView.getChildViewForTask(t);
if (tv == null) {
// TODO: Create a different animation task rect for this case (though it should
diff --git a/packages/SystemUI/src/com/android/systemui/stackdivider/DividerView.java b/packages/SystemUI/src/com/android/systemui/stackdivider/DividerView.java
index 59d4011..a520a33 100644
--- a/packages/SystemUI/src/com/android/systemui/stackdivider/DividerView.java
+++ b/packages/SystemUI/src/com/android/systemui/stackdivider/DividerView.java
@@ -25,8 +25,10 @@
import android.content.res.Configuration;
import android.graphics.Rect;
import android.graphics.Region.Op;
+import android.hardware.display.DisplayManager;
import android.util.AttributeSet;
-import android.util.DisplayMetrics;
+import android.view.Display;
+import android.view.DisplayInfo;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
@@ -262,9 +264,13 @@
}
private void updateDisplayInfo() {
- DisplayMetrics info = mContext.getResources().getDisplayMetrics();
- mDisplayWidth = info.widthPixels;
- mDisplayHeight = info.heightPixels;
+ final DisplayManager displayManager =
+ (DisplayManager) mContext.getSystemService(Context.DISPLAY_SERVICE);
+ Display display = displayManager.getDisplay(Display.DEFAULT_DISPLAY);
+ final DisplayInfo info = new DisplayInfo();
+ display.getDisplayInfo(info);
+ mDisplayWidth = info.logicalWidth;
+ mDisplayHeight = info.logicalHeight;
}
private int calculatePosition(int touchX, int touchY) {
diff --git a/rs/java/android/renderscript/ScriptGroup.java b/rs/java/android/renderscript/ScriptGroup.java
index 54180f4..9bbacbc 100644
--- a/rs/java/android/renderscript/ScriptGroup.java
+++ b/rs/java/android/renderscript/ScriptGroup.java
@@ -278,6 +278,8 @@
public ValueAndSize(RenderScript rs, Object obj) {
if (obj instanceof Allocation) {
value = ((Allocation)obj).getID(rs);
+ // Special value for size to tell the runtime and driver that
+ // the value is an Allocation
size = -1;
} else if (obj instanceof Boolean) {
value = ((Boolean)obj).booleanValue() ? 1 : 0;
@@ -289,10 +291,10 @@
value = ((Long)obj).longValue();
size = 8;
} else if (obj instanceof Float) {
- value = ((Float)obj).longValue();
+ value = Float.floatToRawIntBits(((Float)obj).floatValue());
size = 4;
} else if (obj instanceof Double) {
- value = ((Double)obj).longValue();
+ value = Double.doubleToRawLongBits(((Double)obj).doubleValue());
size = 8;
}
}
diff --git a/rs/jni/android_renderscript_RenderScript.cpp b/rs/jni/android_renderscript_RenderScript.cpp
index be7071e..113241d 100644
--- a/rs/jni/android_renderscript_RenderScript.cpp
+++ b/rs/jni/android_renderscript_RenderScript.cpp
@@ -393,7 +393,6 @@
size_t numValues, numDependencies;
RsScriptFieldID* fieldIDs;
- uintptr_t* values;
RsClosure* depClosures;
RsScriptFieldID* depFieldIDs;
@@ -430,15 +429,6 @@
fieldIDs[i] = (RsScriptFieldID)jFieldIDs[i];
}
- values = (uintptr_t*)alloca(sizeof(uintptr_t) * numValues);
- if (values == nullptr) {
- goto exit;
- }
-
- for (size_t i = 0; i < numValues; i++) {
- values[i] = (uintptr_t)jValues[i];
- }
-
depClosures = (RsClosure*)alloca(sizeof(RsClosure) * numDependencies);
if (depClosures == nullptr) {
goto exit;
@@ -459,7 +449,7 @@
ret = (jlong)(uintptr_t)rsClosureCreate(
(RsContext)con, (RsScriptKernelID)kernelID, (RsAllocation)returnValue,
- fieldIDs, numValues, values, numValues,
+ fieldIDs, numValues, jValues, numValues,
(int*)jSizes, numValues,
depClosures, numDependencies,
depFieldIDs, numDependencies);
@@ -511,7 +501,6 @@
size_t numValues;
RsScriptFieldID* fieldIDs;
- uintptr_t* values;
if (fieldIDs_length != values_length || values_length != sizes_length) {
ALOGE("Unmatched field IDs, values, and sizes in closure creation.");
@@ -534,18 +523,9 @@
fieldIDs[i] = (RsScriptFieldID)jFieldIDs[i];
}
- values = (uintptr_t*)alloca(sizeof(uintptr_t) * numValues);
- if (values == nullptr) {
- goto exit;
- }
-
- for (size_t i = 0; i < numValues; i++) {
- values[i] = (uintptr_t)jValues[i];
- }
-
ret = (jlong)(uintptr_t)rsInvokeClosureCreate(
(RsContext)con, (RsScriptInvokeID)invokeID, jParams, jParamLength,
- fieldIDs, numValues, values, numValues,
+ fieldIDs, numValues, jValues, numValues,
(int*)jSizes, numValues);
exit:
@@ -561,15 +541,17 @@
static void
nClosureSetArg(JNIEnv *_env, jobject _this, jlong con, jlong closureID,
jint index, jlong value, jint size) {
+ // Size is signed with -1 indicating the value is an Allocation
rsClosureSetArg((RsContext)con, (RsClosure)closureID, (uint32_t)index,
- (uintptr_t)value, (size_t)size);
+ (uintptr_t)value, size);
}
static void
nClosureSetGlobal(JNIEnv *_env, jobject _this, jlong con, jlong closureID,
jlong fieldID, jlong value, jint size) {
+ // Size is signed with -1 indicating the value is an Allocation
rsClosureSetGlobal((RsContext)con, (RsClosure)closureID,
- (RsScriptFieldID)fieldID, (uintptr_t)value, (size_t)size);
+ (RsScriptFieldID)fieldID, (int64_t)value, size);
}
static long
diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
index 02b5b8a..25fef18 100644
--- a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
+++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
@@ -79,10 +79,11 @@
import com.android.internal.util.FastXmlSerializer;
import com.android.internal.widget.IRemoteViewsAdapterConnection;
import com.android.internal.widget.IRemoteViewsFactory;
-
import com.android.server.LocalServices;
import com.android.server.WidgetBackupProvider;
+
import libcore.io.IoUtils;
+
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlSerializer;
@@ -1954,8 +1955,13 @@
if (period < MIN_UPDATE_PERIOD) {
period = MIN_UPDATE_PERIOD;
}
- mAlarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
- SystemClock.elapsedRealtime() + period, period, provider.broadcast);
+ final long oldId = Binder.clearCallingIdentity();
+ try {
+ mAlarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
+ SystemClock.elapsedRealtime() + period, period, provider.broadcast);
+ } finally {
+ Binder.restoreCallingIdentity(oldId);
+ }
}
}
}
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index cde126a..566065c 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -12102,8 +12102,6 @@
}
}
- enableSystemUserApps();
-
// Start up initial activity.
mBooting = true;
startHomeActivityLocked(currentUserId, "systemReady");
@@ -12155,43 +12153,6 @@
}
}
- private void enableSystemUserApps() {
- // For system user, enable apps based on the following conditions:
- // - app is whitelisted; or has no launcher icons; or has INTERACT_ACROSS_USERS permission
- // - app is not in the blacklist
- if (UserManager.isSplitSystemUser()) {
- AppsQueryHelper queryHelper = new AppsQueryHelper(mContext);
- Set<String> enableApps = new HashSet<>();
- enableApps.addAll(queryHelper.queryApps(AppsQueryHelper.GET_NON_LAUNCHABLE_APPS
- | AppsQueryHelper.GET_APPS_WITH_INTERACT_ACROSS_USERS_PERM
- | AppsQueryHelper.GET_DEFAULT_IMES,
- /* systemAppsOnly */ true, UserHandle.SYSTEM));
- ArraySet<String> wlApps = SystemConfig.getInstance().getSystemUserWhitelistedApps();
- enableApps.addAll(wlApps);
- ArraySet<String> blApps = SystemConfig.getInstance().getSystemUserBlacklistedApps();
- enableApps.removeAll(blApps);
-
- List<String> systemApps = queryHelper.queryApps(0, /* systemAppsOnly */ true,
- UserHandle.SYSTEM);
- final int systemAppsSize = systemApps.size();
- for (int i = 0; i < systemAppsSize; i++) {
- String pName = systemApps.get(i);
- boolean enable = enableApps.contains(pName);
- try {
- if (enable) {
- AppGlobals.getPackageManager().installExistingPackageAsUser(pName,
- UserHandle.USER_SYSTEM);
- } else {
- AppGlobals.getPackageManager().deletePackageAsUser(pName, null,
- UserHandle.USER_SYSTEM, PackageManager.DELETE_SYSTEM_APP);
- }
- } catch (RemoteException e) {
- Slog.e(TAG, "Error occured when processing package " + pName, e);
- }
- }
- }
- }
-
private boolean makeAppCrashingLocked(ProcessRecord app,
String shortMsg, String longMsg, String stackTrace) {
app.crashing = true;
diff --git a/services/core/java/com/android/server/am/ActivityStack.java b/services/core/java/com/android/server/am/ActivityStack.java
index ba6e9b1c..9896ec5 100644
--- a/services/core/java/com/android/server/am/ActivityStack.java
+++ b/services/core/java/com/android/server/am/ActivityStack.java
@@ -1338,7 +1338,12 @@
return topHomeActivity == null || !topHomeActivity.isHomeActivity();
}
- final int belowFocusedIndex = mStacks.indexOf(focusedStack) - 1;
+ // Find the first stack below focused stack that actually got something visible.
+ int belowFocusedIndex = mStacks.indexOf(focusedStack) - 1;
+ while (belowFocusedIndex >= 0 &&
+ mStacks.get(belowFocusedIndex).topRunningActivityLocked() == null) {
+ belowFocusedIndex--;
+ }
if ((focusedStackId == DOCKED_STACK_ID || focusedStackId == PINNED_STACK_ID)
&& stackIndex == belowFocusedIndex) {
// Stacks directly behind the docked or pinned stack are always visible.
@@ -3103,6 +3108,9 @@
// If the activity is PAUSING, we will complete the finish once
// it is done pausing; else we can just directly finish it here.
if (DEBUG_PAUSE) Slog.v(TAG_PAUSE, "Finish not pausing: " + r);
+ if (r.visible) {
+ mWindowManager.setAppVisibility(r.appToken, false);
+ }
return finishCurrentActivityLocked(r, FINISH_AFTER_PAUSE, oomAdj) == null;
} else {
if (DEBUG_PAUSE) Slog.v(TAG_PAUSE, "Finish waiting for pause of: " + r);
diff --git a/services/core/java/com/android/server/am/ActivityStackSupervisor.java b/services/core/java/com/android/server/am/ActivityStackSupervisor.java
index 4fc8454..13d3ee1 100644
--- a/services/core/java/com/android/server/am/ActivityStackSupervisor.java
+++ b/services/core/java/com/android/server/am/ActivityStackSupervisor.java
@@ -3340,6 +3340,12 @@
return;
}
+ if (task.stack != null && task.stack.mStackId == stackId) {
+ // You are already in the right stack silly...
+ Slog.i(TAG, "moveTaskToStack: taskId=" + taskId + " already in stackId=" + stackId);
+ return;
+ }
+
final ActivityRecord topActivity = task.getTopActivity();
if (StackId.preserveWindowOnTaskMove(stackId) && topActivity != null) {
// We are about to relaunch the activity because its configuration changed due to
@@ -3356,15 +3362,13 @@
// Make sure the task has the appropriate bounds/size for the stack it is in.
if (stackId == FULLSCREEN_WORKSPACE_STACK_ID && task.mBounds != null) {
- resizeTaskLocked(task, stack.mBounds,
- RESIZE_MODE_SYSTEM, !PRESERVE_WINDOWS);
+ resizeTaskLocked(task, stack.mBounds, RESIZE_MODE_SYSTEM, !PRESERVE_WINDOWS);
} else if (stackId == FREEFORM_WORKSPACE_STACK_ID
&& task.mBounds == null && task.mLastNonFullscreenBounds != null) {
resizeTaskLocked(task, task.mLastNonFullscreenBounds,
RESIZE_MODE_SYSTEM, !PRESERVE_WINDOWS);
} else if (stackId == DOCKED_STACK_ID || stackId == PINNED_STACK_ID) {
- resizeTaskLocked(task, stack.mBounds,
- RESIZE_MODE_SYSTEM, !PRESERVE_WINDOWS);
+ resizeTaskLocked(task, stack.mBounds, RESIZE_MODE_SYSTEM, !PRESERVE_WINDOWS);
}
// The task might have already been running and its visibility needs to be synchronized with
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 058d681..fe9fe50 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -1068,6 +1068,7 @@
if (DEBUG_VOL) {
Log.d(TAG, String.format("Master mute %s, user=%d", masterMute, currentUser));
}
+ setSystemAudioMute(masterMute);
AudioSystem.setMasterMute(masterMute);
broadcastMasterMuteStatus(masterMute);
diff --git a/services/core/java/com/android/server/connectivity/Tethering.java b/services/core/java/com/android/server/connectivity/Tethering.java
index c1aaf07..6687412 100644
--- a/services/core/java/com/android/server/connectivity/Tethering.java
+++ b/services/core/java/com/android/server/connectivity/Tethering.java
@@ -1353,18 +1353,9 @@
if (iface != null) {
String[] dnsServers = mDefaultDnsServers;
Collection<InetAddress> dnses = linkProperties.getDnsServers();
- if (dnses != null) {
- // we currently only handle IPv4
- ArrayList<InetAddress> v4Dnses =
- new ArrayList<InetAddress>(dnses.size());
- for (InetAddress dnsAddress : dnses) {
- if (dnsAddress instanceof Inet4Address) {
- v4Dnses.add(dnsAddress);
- }
- }
- if (v4Dnses.size() > 0) {
- dnsServers = NetworkUtils.makeStrings(v4Dnses);
- }
+ if (dnses != null && !dnses.isEmpty()) {
+ // TODO: remove this invocation of NetworkUtils.makeStrings().
+ dnsServers = NetworkUtils.makeStrings(dnses);
}
try {
Network network = getConnectivityManager().getNetworkForType(upType);
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 64628aa..c152514 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -105,6 +105,7 @@
import android.content.ServiceConnection;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
+import android.content.pm.AppsQueryHelper;
import android.content.pm.FeatureInfo;
import android.content.pm.IOnPermissionsChangeListener;
import android.content.pm.IPackageDataObserver;
@@ -1810,10 +1811,48 @@
boolean factoryTest, boolean onlyCore) {
PackageManagerService m = new PackageManagerService(context, installer,
factoryTest, onlyCore);
+ m.enableSystemUserApps();
ServiceManager.addService("package", m);
return m;
}
+ private void enableSystemUserApps() {
+ if (!UserManager.isSplitSystemUser()) {
+ return;
+ }
+ // For system user, enable apps based on the following conditions:
+ // - app is whitelisted or belong to one of these groups:
+ // -- system app which has no launcher icons
+ // -- system app which has INTERACT_ACROSS_USERS permission
+ // -- system IME app
+ // - app is not in the blacklist
+ AppsQueryHelper queryHelper = new AppsQueryHelper(this);
+ Set<String> enableApps = new ArraySet<>();
+ enableApps.addAll(queryHelper.queryApps(AppsQueryHelper.GET_NON_LAUNCHABLE_APPS
+ | AppsQueryHelper.GET_APPS_WITH_INTERACT_ACROSS_USERS_PERM
+ | AppsQueryHelper.GET_IMES, /* systemAppsOnly */ true, UserHandle.SYSTEM));
+ ArraySet<String> wlApps = SystemConfig.getInstance().getSystemUserWhitelistedApps();
+ enableApps.addAll(wlApps);
+ ArraySet<String> blApps = SystemConfig.getInstance().getSystemUserBlacklistedApps();
+ enableApps.removeAll(blApps);
+
+ List<String> systemApps = queryHelper.queryApps(0, /* systemAppsOnly */ true,
+ UserHandle.SYSTEM);
+ final int systemAppsSize = systemApps.size();
+ synchronized (mPackages) {
+ for (int i = 0; i < systemAppsSize; i++) {
+ String pName = systemApps.get(i);
+ PackageSetting pkgSetting = mSettings.mPackages.get(pName);
+ // Should not happen, but we shouldn't be failing if it does
+ if (pkgSetting == null) {
+ continue;
+ }
+ boolean installed = enableApps.contains(pName);
+ pkgSetting.setInstalled(installed, UserHandle.USER_SYSTEM);
+ }
+ }
+ }
+
static String[] splitString(String str, char sep) {
int count = 1;
int i = 0;
@@ -12770,8 +12809,14 @@
ServiceManager.getService(Context.DEVICE_POLICY_SERVICE));
try {
if (dpm != null) {
+ final ComponentName deviceOwnerComponentName = dpm.getDeviceOwner();
+ final String deviceOwnerPackageName = deviceOwnerComponentName == null ? null
+ : deviceOwnerComponentName.getPackageName();
// Does the package contains the device owner?
- if (dpm.isDeviceOwnerPackage(packageName)) {
+ // TODO Do we have to do it even if userId != UserHandle.USER_ALL? Otherwise,
+ // this check is probably not needed, since DO should be registered as a device
+ // admin on some user too. (Original bug for this: b/17657954)
+ if (packageName.equals(deviceOwnerPackageName)) {
return true;
}
// Does it contain a device admin for any user?
diff --git a/services/core/java/com/android/server/webkit/WebViewUpdateService.java b/services/core/java/com/android/server/webkit/WebViewUpdateService.java
index ac79b36..97713fc 100644
--- a/services/core/java/com/android/server/webkit/WebViewUpdateService.java
+++ b/services/core/java/com/android/server/webkit/WebViewUpdateService.java
@@ -54,6 +54,15 @@
@Override
public void onReceive(Context context, Intent intent) {
+ // When a package is replaced we will receive two intents, one representing the
+ // removal of the old package and one representing the addition of the new
+ // package. We here ignore the intent representing the removed package to make
+ // sure we don't change WebView provider twice.
+ if (intent.getAction().equals(Intent.ACTION_PACKAGE_REMOVED)
+ && intent.getExtras().getBoolean(Intent.EXTRA_REPLACING)) {
+ return;
+ }
+
for (String packageName : WebViewFactory.getWebViewPackageNames()) {
String webviewPackage = "package:" + packageName;
@@ -73,7 +82,8 @@
}
};
IntentFilter filter = new IntentFilter();
- filter.addAction(Intent.ACTION_PACKAGE_REPLACED);
+ filter.addAction(Intent.ACTION_PACKAGE_ADDED);
+ filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
filter.addDataScheme("package");
getContext().registerReceiver(mWebViewUpdatedReceiver, filter);
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index f54fd83..e264c43 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -291,16 +291,18 @@
final ArrayList<Task> tasks = mStacks.get(stackNdx).getTasks();
for (int taskNdx = tasks.size() - 1; taskNdx >= 0; --taskNdx) {
final Task task = tasks.get(taskNdx);
- // We need to use the visible frame on the window for any touch-related tests.
- // Can't use the task's bounds because the original task bounds might be adjusted
- // to fit the content frame. For example, the presence of the IME adjusting the
+ final WindowState win = task.getTopVisibleAppMainWindow();
+ if (win == null) {
+ continue;
+ }
+ // We need to use the task's dim bounds (which is derived from the visible
+ // bounds of its apps windows) for any touch-related tests. Can't use
+ // the task's original bounds because it might be adjusted to fit the
+ // content frame. For example, the presence of the IME adjusting the
// windows frames when the app window is the IME target.
- final WindowState win = task.getTopAppMainWindow();
- if (win != null) {
- win.getVisibleBounds(mTmpRect);
- if (mTmpRect.contains(x, y)) {
- return task.mTaskId;
- }
+ task.getDimBounds(mTmpRect);
+ if (mTmpRect.contains(x, y)) {
+ return task.mTaskId;
}
}
}
@@ -308,10 +310,10 @@
}
/**
- * Find the window whose outside touch area (for resizing) (x, y) falls within.
+ * Find the task whose outside touch area (for resizing) (x, y) falls within.
* Returns null if the touch doesn't fall into a resizing area.
*/
- WindowState findWindowForControlPoint(int x, int y) {
+ Task findTaskForControlPoint(int x, int y) {
final int delta = mService.dipToPixel(RESIZE_HANDLE_WIDTH_IN_DP, mDisplayMetrics);
for (int stackNdx = mStacks.size() - 1; stackNdx >= 0; --stackNdx) {
TaskStack stack = mStacks.get(stackNdx);
@@ -325,24 +327,22 @@
return null;
}
- // We need to use the visible frame on the window for any touch-related
- // tests. Can't use the task's bounds because the original task bounds
- // might be adjusted to fit the content frame. (One example is when the
- // task is put to top-left quadrant, the actual visible frame would not
- // start at (0,0) after it's adjusted for the status bar.)
- final WindowState win = task.getTopAppMainWindow();
- if (win != null) {
- win.getVisibleBounds(mTmpRect);
- mTmpRect.inset(-delta, -delta);
- if (mTmpRect.contains(x, y)) {
- mTmpRect.inset(delta, delta);
- if (!mTmpRect.contains(x, y)) {
- return win;
- }
- // User touched inside the task. No need to look further,
- // focus transfer will be handled in ACTION_UP.
- return null;
+ // We need to use the task's dim bounds (which is derived from the visible
+ // bounds of its apps windows) for any touch-related tests. Can't use
+ // the task's original bounds because it might be adjusted to fit the
+ // content frame. One example is when the task is put to top-left quadrant,
+ // the actual visible area would not start at (0,0) after it's adjusted
+ // for the status bar.
+ task.getDimBounds(mTmpRect);
+ mTmpRect.inset(-delta, -delta);
+ if (mTmpRect.contains(x, y)) {
+ mTmpRect.inset(delta, delta);
+ if (!mTmpRect.contains(x, y)) {
+ return task;
}
+ // User touched inside the task. No need to look further,
+ // focus transfer will be handled in ACTION_UP.
+ return null;
}
}
}
@@ -351,12 +351,18 @@
void setTouchExcludeRegion(Task focusedTask) {
mTouchExcludeRegion.set(mBaseDisplayRect);
- WindowList windows = getWindowList();
final int delta = mService.dipToPixel(RESIZE_HANDLE_WIDTH_IN_DP, mDisplayMetrics);
- for (int i = windows.size() - 1; i >= 0; --i) {
- final WindowState win = windows.get(i);
- final Task task = win.getTask();
- if (win.isVisibleLw() && task != null) {
+ boolean addBackFocusedTask = false;
+ for (int stackNdx = mStacks.size() - 1; stackNdx >= 0; --stackNdx) {
+ TaskStack stack = mStacks.get(stackNdx);
+ final ArrayList<Task> tasks = stack.getTasks();
+ for (int taskNdx = tasks.size() - 1; taskNdx >= 0; --taskNdx) {
+ final Task task = tasks.get(taskNdx);
+ final WindowState win = task.getTopVisibleAppMainWindow();
+ if (win == null) {
+ continue;
+ }
+
/**
* Exclusion region is the region that TapDetector doesn't care about.
* Here we want to remove all non-focused tasks from the exclusion region.
@@ -368,13 +374,17 @@
*/
final boolean isFreeformed = task.inFreeformWorkspace();
if (task != focusedTask || isFreeformed) {
- mTmpRect.set(win.mVisibleFrame);
- mTmpRect.intersect(win.mVisibleInsets);
- /**
- * If the task is freeformed, enlarge the area to account for outside
- * touch area for resize.
- */
+ task.getDimBounds(mTmpRect);
if (isFreeformed) {
+ // If we're removing a freeform, focused app from the exclusion region,
+ // we need to add back its touchable frame later. Remember the touchable
+ // frame now.
+ if (task == focusedTask) {
+ addBackFocusedTask = true;
+ mTmpRect2.set(mTmpRect);
+ }
+ // If the task is freeformed, enlarge the area to account for outside
+ // touch area for resize.
mTmpRect.inset(-delta, -delta);
// Intersect with display content rect. If we have system decor (status bar/
// navigation bar), we want to exclude that from the tap detection.
@@ -385,17 +395,14 @@
}
mTouchExcludeRegion.op(mTmpRect, Region.Op.DIFFERENCE);
}
- /**
- * If we removed the focused task above, add it back and only leave its
- * outside touch area in the exclusion. TapDectector is not interested in
- * any touch inside the focused task itself.
- */
- if (task == focusedTask && isFreeformed) {
- mTmpRect.inset(delta, delta);
- mTouchExcludeRegion.op(mTmpRect, Region.Op.UNION);
- }
}
}
+ // If we removed the focused task above, add it back and only leave its
+ // outside touch area in the exclusion. TapDectector is not interested in
+ // any touch inside the focused task itself.
+ if (addBackFocusedTask) {
+ mTouchExcludeRegion.op(mTmpRect2, Region.Op.UNION);
+ }
if (mTapDetector != null) {
mTapDetector.setTouchExcludeRegion(mTouchExcludeRegion);
}
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 1b86488..46cd7cd 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -284,7 +284,12 @@
boolean getMaxVisibleBounds(Rect out) {
boolean foundTop = false;
for (int i = mAppTokens.size() - 1; i >= 0; i--) {
- final WindowState win = mAppTokens.get(i).findMainWindow();
+ final AppWindowToken token = mAppTokens.get(i);
+ // skip hidden (or about to hide) apps
+ if (token.mIsExiting || token.clientHidden || token.hiddenRequested) {
+ continue;
+ }
+ final WindowState win = token.findMainWindow();
if (win == null) {
continue;
}
@@ -413,14 +418,20 @@
return mStack != null && mStack.mStackId == DOCKED_STACK_ID;
}
- WindowState getTopAppMainWindow() {
- final int tokensCount = mAppTokens.size();
- return tokensCount > 0 ? mAppTokens.get(tokensCount - 1).findMainWindow() : null;
+ WindowState getTopVisibleAppMainWindow() {
+ final AppWindowToken token = getTopVisibleAppToken();
+ return token != null ? token.findMainWindow() : null;
}
- AppWindowToken getTopAppWindowToken() {
- final int tokensCount = mAppTokens.size();
- return tokensCount > 0 ? mAppTokens.get(tokensCount - 1) : null;
+ AppWindowToken getTopVisibleAppToken() {
+ for (int i = mAppTokens.size() - 1; i >= 0; i--) {
+ final AppWindowToken token = mAppTokens.get(i);
+ // skip hidden (or about to hide) apps
+ if (!token.mIsExiting && !token.clientHidden && !token.hiddenRequested) {
+ return token;
+ }
+ }
+ return null;
}
@Override
diff --git a/services/core/java/com/android/server/wm/TaskPositioner.java b/services/core/java/com/android/server/wm/TaskPositioner.java
index dd47e7a..f5e4e3b 100644
--- a/services/core/java/com/android/server/wm/TaskPositioner.java
+++ b/services/core/java/com/android/server/wm/TaskPositioner.java
@@ -332,30 +332,34 @@
+ ", {" + startX + ", " + startY + "}");
}
mCtrlType = CTRL_NONE;
+ mTask = win.getTask();
+ mStartDragX = startX;
+ mStartDragY = startY;
+
+ // Use the dim bounds, not the original task bounds. The cursor
+ // movement should be calculated relative to the visible bounds.
+ // Also, use the dim bounds of the task which accounts for
+ // multiple app windows. Don't use any bounds from win itself as it
+ // may not be the same size as the task.
+ mTask.getDimBounds(mTmpRect);
+
if (resize) {
- final Rect visibleFrame = win.mVisibleFrame;
- if (startX < visibleFrame.left) {
+ if (startX < mTmpRect.left) {
mCtrlType |= CTRL_LEFT;
}
- if (startX > visibleFrame.right) {
+ if (startX > mTmpRect.right) {
mCtrlType |= CTRL_RIGHT;
}
- if (startY < visibleFrame.top) {
+ if (startY < mTmpRect.top) {
mCtrlType |= CTRL_TOP;
}
- if (startY > visibleFrame.bottom) {
+ if (startY > mTmpRect.bottom) {
mCtrlType |= CTRL_BOTTOM;
}
mResizing = true;
}
- mTask = win.getTask();
- mStartDragX = startX;
- mStartDragY = startY;
-
- // Use the visible bounds, not the original task bounds. The cursor
- // movement should be calculated relative to the visible bounds.
- mWindowOriginalBounds.set(win.mVisibleFrame);
+ mWindowOriginalBounds.set(mTmpRect);
}
private void endDragLocked() {
diff --git a/services/core/java/com/android/server/wm/TaskTapPointerEventListener.java b/services/core/java/com/android/server/wm/TaskTapPointerEventListener.java
index 1fe359e..f5b83bb 100644
--- a/services/core/java/com/android/server/wm/TaskTapPointerEventListener.java
+++ b/services/core/java/com/android/server/wm/TaskTapPointerEventListener.java
@@ -90,11 +90,11 @@
case MotionEvent.ACTION_HOVER_MOVE: {
final int x = (int) motionEvent.getX();
final int y = (int) motionEvent.getY();
- final WindowState window = mDisplayContent.findWindowForControlPoint(x, y);
- if (window == null) {
+ final Task task = mDisplayContent.findTaskForControlPoint(x, y);
+ if (task == null) {
break;
}
- window.getVisibleBounds(mTmpRect);
+ task.getDimBounds(mTmpRect);
if (!mTmpRect.isEmpty() && !mTmpRect.contains(x, y)) {
int iconShape = STYLE_DEFAULT;
if (x < mTmpRect.left) {
diff --git a/services/core/java/com/android/server/wm/WindowAnimator.java b/services/core/java/com/android/server/wm/WindowAnimator.java
index 38bd71d..a96bd2c 100644
--- a/services/core/java/com/android/server/wm/WindowAnimator.java
+++ b/services/core/java/com/android/server/wm/WindowAnimator.java
@@ -67,7 +67,7 @@
private final WindowSurfacePlacer mWindowPlacerLocked;
/** Is any window animating? */
- boolean mAnimating;
+ private boolean mAnimating;
/** Is any app window animating? */
boolean mAppWindowAnimating;
@@ -168,7 +168,8 @@
appAnimator.wasAnimating = appAnimator.animating;
if (appAnimator.stepAnimationLocked(mCurrentTime, displayId)) {
appAnimator.animating = true;
- mAnimating = mAppWindowAnimating = true;
+ setAnimating(true);
+ mAppWindowAnimating = true;
} else if (appAnimator.wasAnimating) {
// stopped animating, do one more pass through the layout
setAppLayoutChanges(appAnimator,
@@ -186,7 +187,8 @@
final AppWindowAnimator appAnimator = exitingAppTokens.get(i).mAppAnimator;
appAnimator.wasAnimating = appAnimator.animating;
if (appAnimator.stepAnimationLocked(mCurrentTime, displayId)) {
- mAnimating = mAppWindowAnimating = true;
+ setAnimating(true);
+ mAppWindowAnimating = true;
} else if (appAnimator.wasAnimating) {
// stopped animating, do one more pass through the layout
setAppLayoutChanges(appAnimator,
@@ -282,7 +284,7 @@
final boolean wasAnimating = winAnimator.mWasAnimating;
final boolean nowAnimating = winAnimator.stepAnimationLocked(mCurrentTime);
winAnimator.mWasAnimating = nowAnimating;
- mAnimating |= nowAnimating;
+ orAnimating(nowAnimating);
if (DEBUG_WALLPAPER) {
Slog.v(TAG, win + ": wasAnimating=" + wasAnimating +
@@ -546,7 +548,7 @@
}
}
}
- mAnimating = true;
+ setAnimating(true);
}
// If this window's app token is running a detached wallpaper
@@ -617,7 +619,7 @@
// We can now show all of the drawn windows!
if (!mService.mOpeningApps.contains(wtoken)) {
- mAnimating |= appAnimator.showAllWindowsLocked();
+ orAnimating(appAnimator.showAllWindowsLocked());
}
}
}
@@ -636,7 +638,7 @@
mCurrentTime = frameTimeNs / TimeUtils.NANOS_PER_MS;
mBulkUpdateParams = SET_ORIENTATION_CHANGE_COMPLETE;
boolean wasAnimating = mAnimating;
- mAnimating = false;
+ setAnimating(false);
mAppWindowAnimating = false;
if (DEBUG_WINDOW_TRACE) {
Slog.i(TAG, "!!! animate: entry time=" + mCurrentTime);
@@ -657,7 +659,7 @@
displayAnimator.mScreenRotationAnimation;
if (screenRotationAnimation != null && screenRotationAnimation.isAnimating()) {
if (screenRotationAnimation.stepAnimationLocked(mCurrentTime)) {
- mAnimating = true;
+ setAnimating(true);
} else {
mBulkUpdateParams |= SET_UPDATE_ROTATION;
screenRotationAnimation.kill();
@@ -697,7 +699,7 @@
screenRotationAnimation.updateSurfacesInTransaction();
}
- mAnimating |= mService.getDisplayContentLocked(displayId).animateDimLayers();
+ orAnimating(mService.getDisplayContentLocked(displayId).animateDimLayers());
//TODO (multidisplay): Magnification is supported only for the default display.
if (mService.mAccessibilityController != null
@@ -920,4 +922,16 @@
private class DisplayContentsAnimator {
ScreenRotationAnimation mScreenRotationAnimation = null;
}
+
+ boolean isAnimating() {
+ return mAnimating;
+ }
+
+ void setAnimating(boolean animating) {
+ mAnimating = animating;
+ }
+
+ void orAnimating(boolean animating) {
+ mAnimating |= animating;
+ }
}
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index f17698c..92eacd6 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -2737,6 +2737,10 @@
if (win.mAppToken != null) {
win.mAppToken.updateReportedVisibilityLocked();
}
+ if (winAnimator.mReportSurfaceResized) {
+ winAnimator.mReportSurfaceResized = false;
+ result |= WindowManagerGlobal.RELAYOUT_RES_SURFACE_RESIZED;
+ }
outFrame.set(win.mCompatFrame);
outOverscanInsets.set(win.mOverscanInsets);
outContentInsets.set(win.mContentInsets);
@@ -7069,15 +7073,16 @@
}
private void startResizingTask(DisplayContent displayContent, int startX, int startY) {
- WindowState win = null;
+ Task task = null;
synchronized (mWindowMap) {
- win = displayContent.findWindowForControlPoint(startX, startY);
- if (win == null || !startPositioningLocked(win, true /*resize*/, startX, startY)) {
+ task = displayContent.findTaskForControlPoint(startX, startY);
+ if (task == null || !startPositioningLocked(
+ task.getTopVisibleAppMainWindow(), true /*resize*/, startX, startY)) {
return;
}
}
try {
- mActivityManager.setFocusedTask(win.getTask().mTaskId);
+ mActivityManager.setFocusedTask(task.mTaskId);
} catch(RemoteException e) {}
}
@@ -7682,7 +7687,7 @@
synchronized (mWindowMap) {
// Since we're holding both mWindowMap and mAnimator we don't need to
// hold mAnimator.mLayoutToAnim.
- if (mAnimator.mAnimating || mAnimationScheduled) {
+ if (mAnimator.isAnimating() || mAnimationScheduled) {
// If we are animating, don't do the gc now but
// delay a bit so we don't interrupt the animation.
sendEmptyMessageDelayed(H.FORCE_GC, 2000);
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 18bb4ca..673c21f 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -1708,7 +1708,7 @@
Task task = getTask();
if (task == null || task.inHomeStack()
- || task.getTopAppWindowToken() != mAppToken) {
+ || task.getTopVisibleAppToken() != mAppToken) {
// Don't save surfaces for home stack apps. These usually resume and draw
// first frame very fast. Saving surfaces are mostly a waste of memory.
// Don't save if the window is not the topmost window.
diff --git a/services/core/java/com/android/server/wm/WindowStateAnimator.java b/services/core/java/com/android/server/wm/WindowStateAnimator.java
index a3bb320..f7805b1 100644
--- a/services/core/java/com/android/server/wm/WindowStateAnimator.java
+++ b/services/core/java/com/android/server/wm/WindowStateAnimator.java
@@ -16,7 +16,9 @@
package com.android.server.wm;
+import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
+import static android.view.WindowManager.LayoutParams.FLAG_SCALED;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
import static com.android.server.wm.WindowManagerService.DEBUG_ANIM;
import static com.android.server.wm.WindowManagerService.DEBUG_LAYERS;
@@ -24,12 +26,12 @@
import static com.android.server.wm.WindowManagerService.DEBUG_STARTING_WINDOW;
import static com.android.server.wm.WindowManagerService.DEBUG_SURFACE_TRACE;
import static com.android.server.wm.WindowManagerService.DEBUG_VISIBILITY;
-import static com.android.server.wm.WindowManagerService.localLOGV;
import static com.android.server.wm.WindowManagerService.SHOW_LIGHT_TRANSACTIONS;
import static com.android.server.wm.WindowManagerService.SHOW_SURFACE_ALLOC;
import static com.android.server.wm.WindowManagerService.SHOW_TRANSACTIONS;
import static com.android.server.wm.WindowManagerService.TYPE_LAYER_MULTIPLIER;
-import static com.android.server.wm.WindowState.*;
+import static com.android.server.wm.WindowManagerService.localLOGV;
+import static com.android.server.wm.WindowState.DRAG_RESIZE_MODE_FREEFORM;
import static com.android.server.wm.WindowSurfacePlacer.SET_ORIENTATION_CHANGE_COMPLETE;
import static com.android.server.wm.WindowSurfacePlacer.SET_TURN_ON_SCREEN;
@@ -37,7 +39,6 @@
import android.graphics.Matrix;
import android.graphics.PixelFormat;
import android.graphics.Point;
-import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.Region;
import android.os.Debug;
@@ -47,12 +48,10 @@
import android.view.DisplayInfo;
import android.view.MagnificationSpec;
import android.view.Surface.OutOfResourcesException;
-import android.view.Surface;
import android.view.SurfaceControl;
-import android.view.SurfaceSession;
import android.view.WindowManager;
-import android.view.WindowManagerPolicy;
import android.view.WindowManager.LayoutParams;
+import android.view.WindowManagerPolicy;
import android.view.animation.Animation;
import android.view.animation.AnimationSet;
import android.view.animation.AnimationUtils;
@@ -61,7 +60,6 @@
import com.android.server.wm.WindowManagerService.H;
import java.io.PrintWriter;
-import java.util.ArrayList;
/**
* Keep track of animations and surface operations for a single WindowState.
@@ -101,6 +99,11 @@
* we must tell them application to resize (and thus redraw itself).
*/
boolean mSurfaceResized;
+ /**
+ * Whether we should inform the client on next relayoutWindow that
+ * the surface has been resized since last time.
+ */
+ boolean mReportSurfaceResized;
WindowSurfaceController mSurfaceController;
private WindowSurfaceController mPendingDestroySurface;
@@ -183,6 +186,8 @@
int mAttrType;
+ private final Rect mTmpSize = new Rect();
+
WindowStateAnimator(final WindowState win) {
final WindowManagerService service = win.mService;
@@ -442,7 +447,7 @@
if (!isWindowAnimating()) {
//TODO (multidisplay): Accessibility is supported only for the default display.
if (mService.mAccessibilityController != null
- && mWin.getDisplayId() == Display.DEFAULT_DISPLAY) {
+ && mWin.getDisplayId() == DEFAULT_DISPLAY) {
mService.mAccessibilityController.onSomeWindowResizedOrMovedLocked();
}
}
@@ -496,7 +501,7 @@
}
if (mDrawState == DRAW_PENDING) {
if (DEBUG_SURFACE_TRACE || DEBUG_ANIM || SHOW_TRANSACTIONS || DEBUG_ORIENTATION)
- Slog.v(TAG, "finishDrawingLocked: mDrawState=COMMIT_DRAW_PENDING " + this + " in "
+ Slog.v(TAG, "finishDrawingLocked: mDrawState=COMMIT_DRAW_PENDING " + mWin + " in "
+ mSurfaceController);
if (DEBUG_STARTING_WINDOW && startingWindow) {
Slog.v(TAG, "Draw state now committed in " + mWin);
@@ -528,7 +533,7 @@
mWin.mAttrs.type == TYPE_APPLICATION_STARTING) {
result = performShowLocked();
}
- if (mDestroyPreservedSurfaceUponRedraw && result) {
+ if (mDestroyPreservedSurfaceUponRedraw) {
mService.mDestroyPreservedSurface.add(mWin);
}
return result;
@@ -581,52 +586,16 @@
flags |= SurfaceControl.SECURE;
}
- float left = w.mFrame.left + w.mXOffset;
- float top = w.mFrame.top + w.mYOffset;
-
- int width;
- int height;
- if ((attrs.flags & LayoutParams.FLAG_SCALED) != 0) {
- // for a scaled surface, we always want the requested
- // size.
- width = w.mRequestedWidth;
- height = w.mRequestedHeight;
- } else {
- // When we're doing a drag-resizing, request a surface that's fullscreen size,
- // so that we don't need to reallocate during the process. This also prevents
- // buffer drops due to size mismatch.
- final DisplayInfo displayInfo = w.getDisplayInfo();
- if (displayInfo != null && w.isDragResizing()) {
- left = 0;
- top = 0;
- width = displayInfo.logicalWidth;
- height = displayInfo.logicalHeight;
- } else {
- width = w.mCompatFrame.width();
- height = w.mCompatFrame.height();
- }
- }
-
- // Something is wrong and SurfaceFlinger will not like this,
- // try to revert to sane values
- if (width <= 0) {
- width = 1;
- }
- if (height <= 0) {
- height = 1;
- }
-
- // Adjust for surface insets.
- width += attrs.surfaceInsets.left + attrs.surfaceInsets.right;
- height += attrs.surfaceInsets.top + attrs.surfaceInsets.bottom;
- left -= attrs.surfaceInsets.left;
- top -= attrs.surfaceInsets.top;
+ mTmpSize.set(w.mFrame.left + w.mXOffset, w.mFrame.top + w.mYOffset, 0, 0);
+ calculateSurfaceBounds(w, attrs);
+ final int width = mTmpSize.width();
+ final int height = mTmpSize.height();
if (DEBUG_VISIBILITY) {
Slog.v(TAG, "Creating surface in session "
+ mSession.mSurfaceSession + " window " + this
+ " w=" + width + " h=" + height
- + " x=" + left + " y=" + top
+ + " x=" + mTmpSize.left + " y=" + mTmpSize.top
+ " format=" + attrs.format + " flags=" + flags);
}
@@ -692,15 +661,15 @@
Slog.i(TAG, ">>> OPEN TRANSACTION createSurfaceLocked");
WindowManagerService.logSurface(w, "CREATE pos=("
+ w.mFrame.left + "," + w.mFrame.top + ") ("
- + w.mCompatFrame.width() + "x" + w.mCompatFrame.height()
- + "), layer=" + mAnimLayer + " HIDE", null);
+ + width + "x" + height + "), layer=" + mAnimLayer + " HIDE", null);
}
// Start a new transaction and apply position & offset.
final int layerStack = w.getDisplayContent().getDisplay().getLayerStack();
if (WindowManagerService.SHOW_TRANSACTIONS) WindowManagerService.logSurface(w,
- "POS " + left + ", " + top, null);
- mSurfaceController.setPositionAndLayer(left, top, layerStack, mAnimLayer);
+ "POS " + mTmpSize.left + ", " + mTmpSize.top, null);
+ mSurfaceController.setPositionAndLayer(mTmpSize.left, mTmpSize.top, layerStack,
+ mAnimLayer);
mLastHidden = true;
if (WindowManagerService.localLOGV) Slog.v(
@@ -709,6 +678,57 @@
return mSurfaceController;
}
+ private void calculateSurfaceBounds(WindowState w, LayoutParams attrs) {
+ if ((attrs.flags & FLAG_SCALED) != 0) {
+ // For a scaled surface, we always want the requested size.
+ mTmpSize.right = mTmpSize.left + w.mRequestedWidth;
+ mTmpSize.bottom = mTmpSize.top + w.mRequestedHeight;
+ } else {
+ // When we're doing a drag-resizing, request a surface that's fullscreen size,
+ // so that we don't need to reallocate during the process. This also prevents
+ // buffer drops due to size mismatch.
+ if (w.isDragResizing()) {
+ if (w.getResizeMode() == DRAG_RESIZE_MODE_FREEFORM) {
+ mTmpSize.left = 0;
+ mTmpSize.top = 0;
+ }
+ final DisplayInfo displayInfo = w.getDisplayInfo();
+ mTmpSize.right = mTmpSize.left + displayInfo.logicalWidth;
+ mTmpSize.bottom = mTmpSize.top + displayInfo.logicalHeight;
+ } else {
+ mTmpSize.right = mTmpSize.left + w.mCompatFrame.width();
+ mTmpSize.bottom = mTmpSize.top + w.mCompatFrame.height();
+ }
+ }
+
+ // Something is wrong and SurfaceFlinger will not like this, try to revert to sane values.
+ if (mTmpSize.width() < 1) {
+ Slog.w(TAG, "Width of " + w + " is not positive " + mTmpSize.width());
+ mTmpSize.right = mTmpSize.left + 1;
+ }
+ if (mTmpSize.height() < 1) {
+ Slog.w(TAG, "Height of " + w + " is not positive " + mTmpSize.height());
+ mTmpSize.bottom = mTmpSize.top + 1;
+ }
+
+ final int displayId = w.getDisplayId();
+ float scale = 1.0f;
+ // Magnification is supported only for the default display.
+ if (mService.mAccessibilityController != null && displayId == DEFAULT_DISPLAY) {
+ final MagnificationSpec spec =
+ mService.mAccessibilityController.getMagnificationSpecForWindowLocked(w);
+ if (spec != null && !spec.isNop()) {
+ scale = spec.scale;
+ }
+ }
+
+ // Adjust for surface insets.
+ mTmpSize.left -= scale * attrs.surfaceInsets.left;
+ mTmpSize.top -= scale * attrs.surfaceInsets.top;
+ mTmpSize.right += scale * (attrs.surfaceInsets.left + attrs.surfaceInsets.right);
+ mTmpSize.bottom += scale * (attrs.surfaceInsets.top + attrs.surfaceInsets.bottom);
+ }
+
void destroySurfaceLocked() {
final AppWindowToken wtoken = mWin.mAppToken;
if (wtoken != null) {
@@ -891,7 +911,7 @@
tmpMatrix.postTranslate(frame.left + mWin.mXOffset, frame.top + mWin.mYOffset);
//TODO (multidisplay): Magnification is supported only for the default display.
- if (mService.mAccessibilityController != null && displayId == Display.DEFAULT_DISPLAY) {
+ if (mService.mAccessibilityController != null && displayId == DEFAULT_DISPLAY) {
MagnificationSpec spec = mService.mAccessibilityController
.getMagnificationSpecForWindowLocked(mWin);
if (spec != null && !spec.isNop()) {
@@ -974,7 +994,7 @@
MagnificationSpec spec = null;
//TODO (multidisplay): Magnification is supported only for the default display.
- if (mService.mAccessibilityController != null && displayId == Display.DEFAULT_DISPLAY) {
+ if (mService.mAccessibilityController != null && displayId == DEFAULT_DISPLAY) {
spec = mService.mAccessibilityController.getMagnificationSpecForWindowLocked(mWin);
}
if (spec != null) {
@@ -1069,7 +1089,7 @@
} else if (w.mDecorFrame.isEmpty()) {
// Windows without policy decor aren't cropped.
w.mSystemDecorRect.set(0, 0, w.mCompatFrame.width(), w.mCompatFrame.height());
- } else if (w.mAttrs.type == LayoutParams.TYPE_WALLPAPER && mAnimator.mAnimating) {
+ } else if (w.mAttrs.type == LayoutParams.TYPE_WALLPAPER && mAnimator.isAnimating()) {
// If we're animating, the wallpaper crop should only be updated at the end of the
// animation.
mTmpClipRect.set(w.mSystemDecorRect);
@@ -1109,12 +1129,8 @@
// The clip rect was generated assuming (0,0) as the window origin,
// so we need to translate to match the actual surface coordinates.
clipRect.offset(attrs.surfaceInsets.left, attrs.surfaceInsets.top);
- // We don't want to clip to stack bounds windows that are currently doing entrance
- // animation for docked window, otherwise the animating window will be suddenly cut off.
- if (!(mAnimator.mAnimating && w.inDockedWorkspace())) {
- adjustCropToStackBounds(w, clipRect, isFreeformResizing);
- }
+ adjustCropToStackBounds(w, clipRect, isFreeformResizing);
w.transformFromScreenToSurfaceSpace(clipRect);
@@ -1127,100 +1143,57 @@
private void adjustCropToStackBounds(WindowState w, Rect clipRect, boolean isFreeformResizing) {
final AppWindowToken appToken = w.mAppToken;
final Task task = w.getTask();
- // We don't apply the the stack bounds to the window that is being replaced, because it was
- // living in a different stack. If we suddenly crop it to the new stack bounds, it might
- // get cut off. We don't want it to happen, so we let it ignore the stack bounds until it
- // gets removed. The window that will replace it will abide them.
- if (task != null && appToken.mCropWindowsToStack && !appToken.mWillReplaceWindow) {
- TaskStack stack = task.mStack;
- stack.getDimBounds(mTmpStackBounds);
- // When we resize we use the big surface approach, which means we can't trust the
- // window frame bounds anymore. Instead, the window will be placed at 0, 0, but to avoid
- // hardcoding it, we use surface coordinates.
- final int frameX = isFreeformResizing ? (int) mSurfaceController.getX() :
- w.mFrame.left + mWin.mXOffset - w.getAttrs().surfaceInsets.left;
- final int frameY = isFreeformResizing ? (int) mSurfaceController.getY() :
- w.mFrame.top + mWin.mYOffset - w.getAttrs().surfaceInsets.top;
- // We need to do some acrobatics with surface position, because their clip region is
- // relative to the inside of the surface, but the stack bounds aren't.
- clipRect.left = Math.max(0,
- Math.max(mTmpStackBounds.left, frameX + clipRect.left) - frameX);
- clipRect.top = Math.max(0,
- Math.max(mTmpStackBounds.top, frameY + clipRect.top) - frameY);
- clipRect.right = Math.max(0,
- Math.min(mTmpStackBounds.right, frameX + clipRect.right) - frameX);
- clipRect.bottom = Math.max(0,
- Math.min(mTmpStackBounds.bottom, frameY + clipRect.bottom) - frameY);
+ if (task == null || !appToken.mCropWindowsToStack) {
+ return;
}
+
+ // We don't apply the stack bounds crop if:
+ // 1. The window is currently animating docked mode, otherwise the animating window will be
+ // suddenly cut off.
+ // 2. The window that is being replaced during animation, because it was living in a
+ // different stack. If we suddenly crop it to the new stack bounds, it might get cut off.
+ // We don't want it to happen, so we let it ignore the stack bounds until it gets removed.
+ // The window that will replace it will abide them.
+ if (isAnimating() && (appToken.mWillReplaceWindow || w.inDockedWorkspace())) {
+ return;
+ }
+
+ final TaskStack stack = task.mStack;
+ stack.getDimBounds(mTmpStackBounds);
+ // When we resize we use the big surface approach, which means we can't trust the
+ // window frame bounds anymore. Instead, the window will be placed at 0, 0, but to avoid
+ // hardcoding it, we use surface coordinates.
+ final int frameX = isFreeformResizing ? (int) mSurfaceController.getX() :
+ w.mFrame.left + mWin.mXOffset - w.getAttrs().surfaceInsets.left;
+ final int frameY = isFreeformResizing ? (int) mSurfaceController.getY() :
+ w.mFrame.top + mWin.mYOffset - w.getAttrs().surfaceInsets.top;
+ // We need to do some acrobatics with surface position, because their clip region is
+ // relative to the inside of the surface, but the stack bounds aren't.
+ clipRect.left = Math.max(0,
+ Math.max(mTmpStackBounds.left, frameX + clipRect.left) - frameX);
+ clipRect.top = Math.max(0,
+ Math.max(mTmpStackBounds.top, frameY + clipRect.top) - frameY);
+ clipRect.right = Math.max(0,
+ Math.min(mTmpStackBounds.right, frameX + clipRect.right) - frameX);
+ clipRect.bottom = Math.max(0,
+ Math.min(mTmpStackBounds.bottom, frameY + clipRect.bottom) - frameY);
}
void setSurfaceBoundariesLocked(final boolean recoveringMemory) {
final WindowState w = mWin;
- float left = w.mShownPosition.x;
- float top = w.mShownPosition.y;
+ mTmpSize.set(w.mShownPosition.x, w.mShownPosition.y, 0, 0);
+ calculateSurfaceBounds(w, w.getAttrs());
- int width;
- int height;
- if ((w.mAttrs.flags & LayoutParams.FLAG_SCALED) != 0) {
- // for a scaled surface, we always want the requested
- // size.
- width = w.mRequestedWidth;
- height = w.mRequestedHeight;
- } else {
- // When we're doing a drag-resizing, request a surface that's fullscreen size,
- // so that we don't need to reallocate during the process. This also prevents
- // buffer drops due to size mismatch.
- final DisplayInfo displayInfo = w.getDisplayInfo();
-
- // In freeform resize mode, put surface at 0/0.
- if (w.isDragResizing() && w.getResizeMode() == DRAG_RESIZE_MODE_FREEFORM) {
- left = 0;
- top = 0;
- }
- if (displayInfo != null && w.isDragResizing()) {
- width = displayInfo.logicalWidth;
- height = displayInfo.logicalHeight;
- } else {
- width = w.mCompatFrame.width();
- height = w.mCompatFrame.height();
- }
- }
-
- // Something is wrong and SurfaceFlinger will not like this,
- // try to revert to sane values
- if (width < 1) {
- width = 1;
- }
- if (height < 1) {
- height = 1;
- }
-
- // Adjust for surface insets.
- final LayoutParams attrs = w.getAttrs();
- final int displayId = w.getDisplayId();
- float scale = 1.0f;
- // Magnification is supported only for the default display.
- if (mService.mAccessibilityController != null && displayId == Display.DEFAULT_DISPLAY) {
- MagnificationSpec spec =
- mService.mAccessibilityController.getMagnificationSpecForWindowLocked(w);
- if (spec != null && !spec.isNop()) {
- scale = spec.scale;
- }
- }
-
- width += scale * (attrs.surfaceInsets.left + attrs.surfaceInsets.right);
- height += scale * (attrs.surfaceInsets.top + attrs.surfaceInsets.bottom);
- left -= scale * attrs.surfaceInsets.left;
- top -= scale * attrs.surfaceInsets.top;
-
- mSurfaceController.setPositionInTransaction(left, top, recoveringMemory);
- mSurfaceResized = mSurfaceController.setSizeInTransaction(width, height,
+ mSurfaceController.setPositionInTransaction(mTmpSize.left, mTmpSize.top, recoveringMemory);
+ mSurfaceResized = mSurfaceController.setSizeInTransaction(
+ mTmpSize.width(), mTmpSize.height(),
mDsDx * w.mHScale, mDtDx * w.mVScale,
mDsDy * w.mHScale, mDtDy * w.mVScale,
recoveringMemory);
if (mSurfaceResized) {
+ mReportSurfaceResized = true;
mAnimator.setPendingLayoutChanges(w.getDisplayId(),
WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER);
w.applyDimLayerIfNeeded();
@@ -1449,7 +1422,7 @@
// Force the show in the next prepareSurfaceLocked() call.
mLastAlpha = -1;
if (DEBUG_SURFACE_TRACE || DEBUG_ANIM)
- Slog.v(TAG, "performShowLocked: mDrawState=HAS_DRAWN in " + this);
+ Slog.v(TAG, "performShowLocked: mDrawState=HAS_DRAWN in " + mWin);
mDrawState = HAS_DRAWN;
mService.scheduleAnimationLocked();
@@ -1532,7 +1505,7 @@
applyAnimationLocked(transit, true);
//TODO (multidisplay): Magnification is supported only for the default display.
if (mService.mAccessibilityController != null
- && mWin.getDisplayId() == Display.DEFAULT_DISPLAY) {
+ && mWin.getDisplayId() == DEFAULT_DISPLAY) {
mService.mAccessibilityController.onWindowTransitionLocked(mWin, transit);
}
}
diff --git a/services/core/java/com/android/server/wm/WindowSurfacePlacer.java b/services/core/java/com/android/server/wm/WindowSurfacePlacer.java
index 3b57634..ae96658 100644
--- a/services/core/java/com/android/server/wm/WindowSurfacePlacer.java
+++ b/services/core/java/com/android/server/wm/WindowSurfacePlacer.java
@@ -542,11 +542,8 @@
mService.scheduleAnimationLocked();
- if (DEBUG_WINDOW_TRACE) {
- Slog.e(TAG,
- "performSurfacePlacementInner exit: animating="
- + mService.mAnimator.mAnimating);
- }
+ if (DEBUG_WINDOW_TRACE) Slog.e(TAG,
+ "performSurfacePlacementInner exit: animating=" + mService.mAnimator.isAnimating());
}
private void applySurfaceChangesTransaction(boolean recoveringMemory, int numDisplays,
@@ -1184,7 +1181,7 @@
">>> OPEN TRANSACTION handleAppTransitionReadyLocked()");
SurfaceControl.openTransaction();
try {
- mService.mAnimator.mAnimating |= appAnimator.showAllWindowsLocked();
+ mService.mAnimator.orAnimating(appAnimator.showAllWindowsLocked());
} finally {
SurfaceControl.closeTransaction();
if (SHOW_LIGHT_TRANSACTIONS) Slog.i(TAG,
@@ -1467,7 +1464,7 @@
appAnimator.mAllAppWinAnimators.add(wtoken.allAppWindows.get(j).mWinAnimator);
}
mService.mAnimator.mAppWindowAnimating |= appAnimator.isAnimating();
- mService.mAnimator.mAnimating |= appAnimator.showAllWindowsLocked();
+ mService.mAnimator.orAnimating(appAnimator.showAllWindowsLocked());
}
}
}
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 6c2bd00..c611503 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -1612,25 +1612,17 @@
@VisibleForTesting
boolean isActiveAdminWithPolicyForUserLocked(ActiveAdmin admin, int reqPolicy,
int userId) {
- boolean ownsDevice = isDeviceOwner(admin.info.getComponent());
- boolean ownsProfile = (getProfileOwner(userId) != null
- && getProfileOwner(userId).getPackageName()
- .equals(admin.info.getPackageName()));
+ final boolean ownsDevice = isDeviceOwner(admin.info.getComponent(), userId);
+ final boolean ownsProfile = isProfileOwner(admin.info.getComponent(), userId);
if (reqPolicy == DeviceAdminInfo.USES_POLICY_DEVICE_OWNER) {
- if ((userId == UserHandle.USER_SYSTEM && ownsDevice) || (ownsDevice && ownsProfile)) {
- return true;
- }
+ return ownsDevice;
} else if (reqPolicy == DeviceAdminInfo.USES_POLICY_PROFILE_OWNER) {
- if ((userId == UserHandle.USER_SYSTEM && ownsDevice) || ownsProfile) {
- return true;
- }
+ // DO always has the PO power.
+ return ownsDevice || ownsProfile;
} else {
- if (admin.info.usesPolicy(reqPolicy)) {
- return true;
- }
+ return admin.info.usesPolicy(reqPolicy);
}
- return false;
}
void sendAdminCommandLocked(ActiveAdmin admin, String action) {
@@ -2441,8 +2433,9 @@
return;
}
if (admin.getUid() != mInjector.binderGetCallingUid()) {
- // Active device owners must remain active admins.
- if (isDeviceOwner(adminReceiver)) {
+ // Active device/profile owners must remain active admins.
+ if (isDeviceOwner(adminReceiver, userHandle)
+ || isProfileOwner(adminReceiver, userHandle)) {
return;
}
mContext.enforceCallingOrSelfPermission(
@@ -3187,12 +3180,21 @@
}
@Override
- public boolean resetPassword(String passwordOrNull, int flags) {
+ public boolean resetPassword(String passwordOrNull, int flags) throws RemoteException {
if (!mHasFeature) {
return false;
}
final int userHandle = UserHandle.getCallingUserId();
- enforceNotManagedProfile(userHandle, "reset the password");
+
+ long ident = mInjector.binderClearCallingIdentity();
+ try {
+ if (mUserManager.getCredentialOwnerProfile(userHandle) != userHandle) {
+ throw new SecurityException("You can not change password for this profile because"
+ + " it shares the password with the owner profile");
+ }
+ } finally {
+ mInjector.binderRestoreCallingIdentity(ident);
+ }
String password = passwordOrNull != null ? passwordOrNull : "";
@@ -3200,8 +3202,35 @@
synchronized (this) {
// This api can only be called by an active device admin,
// so try to retrieve it to check that the caller is one.
- getActiveAdminForCallerLocked(null,
+ final ActiveAdmin admin = getActiveAdminForCallerLocked(null,
DeviceAdminInfo.USES_POLICY_RESET_PASSWORD);
+ final ComponentName adminComponent = admin.info.getComponent();
+
+ // As of N, only profile owners and device owners can reset the password.
+ if (!(isProfileOwner(adminComponent, userHandle)
+ || isDeviceOwner(adminComponent, userHandle))) {
+ final boolean preN = getTargetSdk(admin.info.getPackageName(), userHandle)
+ < android.os.Build.VERSION_CODES.N;
+ // As of N, password resetting to empty/null is not allowed anymore.
+ // TODO Should we allow DO/PO to set an empty password?
+ if (TextUtils.isEmpty(password)) {
+ if (!preN) {
+ throw new SecurityException("Cannot call with null password");
+ } else {
+ Slog.e(LOG_TAG, "Cannot call with null password");
+ return false;
+ }
+ }
+ // As of N, password cannot be changed by the admin if it is already set.
+ if (isLockScreenSecureUnchecked(userHandle)) {
+ if (!preN) {
+ throw new SecurityException("Admin cannot change current password");
+ } else {
+ Slog.e(LOG_TAG, "Admin cannot change current password");
+ return false;
+ }
+ }
+ }
quality = getPasswordQuality(null, userHandle);
if (quality != DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED) {
int realQuality = LockPatternUtils.computePasswordQuality(password);
@@ -3303,9 +3332,9 @@
// Don't do this with the lock held, because it is going to call
// back in to the service.
- long ident = mInjector.binderClearCallingIdentity();
+ ident = mInjector.binderClearCallingIdentity();
try {
- LockPatternUtils utils = new LockPatternUtils(mContext);
+ LockPatternUtils utils = mInjector.newLockPatternUtils();
if (!TextUtils.isEmpty(password)) {
utils.saveLockPassword(password, null, quality, userHandle);
} else {
@@ -3330,6 +3359,15 @@
return true;
}
+ private boolean isLockScreenSecureUnchecked(int userId) {
+ long ident = mInjector.binderClearCallingIdentity();
+ try {
+ return mInjector.newLockPatternUtils().isSecure(userId);
+ } finally {
+ mInjector.binderRestoreCallingIdentity(ident);
+ }
+ }
+
private void setDoNotAskCredentialsOnBoot() {
synchronized (this) {
DevicePolicyData policyData = getUserData(UserHandle.USER_SYSTEM);
@@ -3685,10 +3723,11 @@
}
@Override
- public void wipeData(int flags, final int userHandle) {
+ public void wipeData(int flags) {
if (!mHasFeature) {
return;
}
+ final int userHandle = mInjector.userHandleGetCallingUserId();
enforceCrossUserPermission(userHandle);
synchronized (this) {
// This API can only be called by an active device admin,
@@ -3701,8 +3740,7 @@
long ident = mInjector.binderClearCallingIdentity();
try {
if ((flags & WIPE_RESET_PROTECTION_DATA) != 0) {
- if (userHandle != UserHandle.USER_SYSTEM
- || !isDeviceOwner(admin.info.getComponent())) {
+ if (!isDeviceOwner(admin.info.getComponent(), userHandle)) {
throw new SecurityException(
"Only device owner admins can set WIPE_RESET_PROTECTION_DATA");
}
@@ -4325,7 +4363,7 @@
return;
}
Preconditions.checkNotNull(who, "ComponentName is null");
- final int userHandle = UserHandle.getCallingUserId();
+ final int userHandle = mInjector.userHandleGetCallingUserId();
synchronized (this) {
ActiveAdmin ap = getActiveAdminForCallerLocked(who,
DeviceAdminInfo.USES_POLICY_DISABLE_CAMERA);
@@ -4337,7 +4375,7 @@
// Tell the user manager that the restrictions have changed.
synchronized (mUserManagerInternal.getUserRestrictionsLock()) {
synchronized (this) {
- if (isDeviceOwner(who)) {
+ if (isDeviceOwner(who, userHandle)) {
mUserManagerInternal.updateEffectiveUserRestrictionsForAllUsersLR();
} else {
mUserManagerInternal.updateEffectiveUserRestrictionsLR(userHandle);
@@ -4499,24 +4537,17 @@
}
}
- public boolean isDeviceOwner(ComponentName who) {
- if (!mHasFeature) {
- return false;
- }
+ public boolean isDeviceOwner(ComponentName who, int userId) {
synchronized (this) {
- return mOwners.hasDeviceOwner() && mOwners.getDeviceOwnerComponent().equals(who);
+ return mOwners.hasDeviceOwner()
+ && mOwners.getDeviceOwnerUserId() == userId
+ && mOwners.getDeviceOwnerComponent().equals(who);
}
}
- @Override
- public boolean isDeviceOwnerPackage(String packageName) {
- if (!mHasFeature) {
- return false;
- }
- synchronized (this) {
- return mOwners.hasDeviceOwner()
- && mOwners.getDeviceOwnerComponent().getPackageName().equals(packageName);
- }
+ public boolean isProfileOwner(ComponentName who, int userId) {
+ final ComponentName profileOwner = getProfileOwner(userId);
+ return who != null && who.equals(profileOwner);
}
@Override
@@ -5637,9 +5668,8 @@
ActiveAdmin activeAdmin =
getActiveAdminForCallerLocked(who,
DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
- final boolean isDeviceOwner = isDeviceOwner(who);
- if (!isDeviceOwner && userHandle != UserHandle.USER_SYSTEM
- && DEVICE_OWNER_USER_RESTRICTIONS.contains(key)) {
+ final boolean isDeviceOwner = isDeviceOwner(who, userHandle);
+ if (!isDeviceOwner && DEVICE_OWNER_USER_RESTRICTIONS.contains(key)) {
throw new SecurityException(
"Profile owners cannot set user restriction " + key);
}
@@ -6132,9 +6162,8 @@
Bundle adminExtras = new Bundle();
adminExtras.putString(DeviceAdminReceiver.EXTRA_LOCK_TASK_PACKAGE, pkg);
for (ActiveAdmin admin : policy.mAdminList) {
- boolean ownsDevice = isDeviceOwner(admin.info.getComponent());
- boolean ownsProfile = (getProfileOwner(userHandle) != null
- && getProfileOwner(userHandle).equals(admin.info.getPackageName()));
+ final boolean ownsDevice = isDeviceOwner(admin.info.getComponent(), userHandle);
+ final boolean ownsProfile = isProfileOwner(admin.info.getComponent(), userHandle);
if (ownsDevice || ownsProfile) {
if (isEnabled) {
sendAdminCommandLocked(admin, DeviceAdminReceiver.ACTION_LOCK_TASK_ENTERING,
@@ -6186,13 +6215,12 @@
@Override
public void setSecureSetting(ComponentName who, String setting, String value) {
Preconditions.checkNotNull(who, "ComponentName is null");
- int callingUserId = UserHandle.getCallingUserId();
- final ContentResolver contentResolver = mContext.getContentResolver();
+ int callingUserId = mInjector.userHandleGetCallingUserId();
synchronized (this) {
getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
- if (isDeviceOwner(who)) {
+ if (isDeviceOwner(who, mInjector.userHandleGetCallingUserId())) {
if (!SECURE_SETTINGS_DEVICEOWNER_WHITELIST.contains(setting)) {
throw new SecurityException(String.format(
"Permission denial: Device owners cannot update %1$s", setting));
@@ -6504,13 +6532,26 @@
* @param callerUid UID of the caller.
* @return true if the caller is the device owner app
*/
- private boolean isCallerDeviceOwner(int callerUid) {
- String[] pkgs = mContext.getPackageManager().getPackagesForUid(callerUid);
- for (String pkg : pkgs) {
- if (isDeviceOwnerPackage(pkg)) {
- return true;
+ @VisibleForTesting
+ boolean isCallerDeviceOwner(int callerUid) {
+ synchronized (this) {
+ if (!mOwners.hasDeviceOwner()) {
+ return false;
+ }
+ if (UserHandle.getUserId(callerUid) != mOwners.getDeviceOwnerUserId()) {
+ return false;
+ }
+ final String deviceOwnerPackageName = mOwners.getDeviceOwnerComponent()
+ .getPackageName();
+ final String[] pkgs = mContext.getPackageManager().getPackagesForUid(callerUid);
+
+ for (String pkg : pkgs) {
+ if (deviceOwnerPackageName.equals(pkg)) {
+ return true;
+ }
}
}
+
return false;
}
@@ -6590,10 +6631,8 @@
getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
long ident = mInjector.binderClearCallingIdentity();
try {
- final ApplicationInfo ai = mIPackageManager
- .getApplicationInfo(packageName, 0, user.getIdentifier());
- final int targetSdkVersion = ai == null ? 0 : ai.targetSdkVersion;
- if (targetSdkVersion < android.os.Build.VERSION_CODES.M) {
+ if (getTargetSdk(packageName, user.getIdentifier())
+ < android.os.Build.VERSION_CODES.M) {
return false;
}
final PackageManager packageManager = mContext.getPackageManager();
@@ -6689,23 +6728,48 @@
}
return true;
} else if (DevicePolicyManager.ACTION_PROVISION_MANAGED_DEVICE.equals(action)) {
- if (getProfileOwner(callingUserId) != null) {
- return false;
- }
- if (mInjector.settingsGlobalGetInt(Settings.Global.DEVICE_PROVISIONED, 0) != 0) {
- return false;
- }
- if (callingUserId != UserHandle.USER_SYSTEM) {
- // Device owner provisioning can only be initiated from system user.
- return false;
- }
- return true;
+ return isDeviceOwnerProvisioningAllowed(callingUserId);
} else if (DevicePolicyManager.ACTION_PROVISION_MANAGED_USER.equals(action)) {
+ if (!UserManager.isSplitSystemUser()) {
+ // ACTION_PROVISION_MANAGED_USER only supported on split-user systems.
+ return false;
+ }
if (hasUserSetupCompleted(callingUserId)) {
return false;
}
return true;
+ } else if (DevicePolicyManager.ACTION_PROVISION_MANAGED_SHAREABLE_DEVICE.equals(action)) {
+ if (!UserManager.isSplitSystemUser()) {
+ // ACTION_PROVISION_MANAGED_SHAREABLE_DEVICE only supported on split-user systems.
+ return false;
+ }
+ return isDeviceOwnerProvisioningAllowed(callingUserId);
}
throw new IllegalArgumentException("Unknown provisioning action " + action);
}
+
+ private boolean isDeviceOwnerProvisioningAllowed(int callingUserId) {
+ if (getProfileOwner(callingUserId) != null) {
+ return false;
+ }
+ if (mInjector.settingsGlobalGetInt(Settings.Global.DEVICE_PROVISIONED, 0) != 0) {
+ return false;
+ }
+ if (callingUserId != UserHandle.USER_SYSTEM) {
+ // Device owner provisioning can only be initiated from system user.
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Returns the target sdk version number that the given packageName was built for
+ * in the given user.
+ */
+ private int getTargetSdk(String packageName, int userId) throws RemoteException {
+ final ApplicationInfo ai = mIPackageManager
+ .getApplicationInfo(packageName, 0, userId);
+ final int targetSdkVersion = ai == null ? 0 : ai.targetSdkVersion;
+ return targetSdkVersion;
+ }
}
diff --git a/tools/aapt2/ResourceTable.cpp b/tools/aapt2/ResourceTable.cpp
index deafe20..73d8585 100644
--- a/tools/aapt2/ResourceTable.cpp
+++ b/tools/aapt2/ResourceTable.cpp
@@ -190,22 +190,32 @@
bool ResourceTable::addResource(const ResourceNameRef& name, const ConfigDescription& config,
std::unique_ptr<Value> value, IDiagnostics* diag) {
- return addResourceImpl(name, {}, config, std::move(value), kValidNameChars, diag);
+ return addResourceImpl(name, {}, config, std::move(value), kValidNameChars,
+ resolveValueCollision, diag);
}
bool ResourceTable::addResource(const ResourceNameRef& name, const ResourceId resId,
const ConfigDescription& config, std::unique_ptr<Value> value,
IDiagnostics* diag) {
- return addResourceImpl(name, resId, config, std::move(value), kValidNameChars, diag);
+ return addResourceImpl(name, resId, config, std::move(value), kValidNameChars,
+ resolveValueCollision, diag);
}
bool ResourceTable::addFileReference(const ResourceNameRef& name, const ConfigDescription& config,
const Source& source, const StringPiece16& path,
IDiagnostics* diag) {
+ return addFileReference(name, config, source, path, resolveValueCollision, diag);
+}
+
+bool ResourceTable::addFileReference(const ResourceNameRef& name, const ConfigDescription& config,
+ const Source& source, const StringPiece16& path,
+ std::function<int(Value*,Value*)> conflictResolver,
+ IDiagnostics* diag) {
std::unique_ptr<FileReference> fileRef = util::make_unique<FileReference>(
stringPool.makeRef(path));
fileRef->setSource(source);
- return addResourceImpl(name, ResourceId{}, config, std::move(fileRef), kValidNameChars, diag);
+ return addResourceImpl(name, ResourceId{}, config, std::move(fileRef), kValidNameChars,
+ conflictResolver, diag);
}
bool ResourceTable::addResourceAllowMangled(const ResourceNameRef& name,
@@ -213,7 +223,7 @@
std::unique_ptr<Value> value,
IDiagnostics* diag) {
return addResourceImpl(name, ResourceId{}, config, std::move(value), kValidNameMangledChars,
- diag);
+ resolveValueCollision, diag);
}
bool ResourceTable::addResourceAllowMangled(const ResourceNameRef& name,
@@ -221,12 +231,17 @@
const ConfigDescription& config,
std::unique_ptr<Value> value,
IDiagnostics* diag) {
- return addResourceImpl(name, id, config, std::move(value), kValidNameMangledChars, diag);
+ return addResourceImpl(name, id, config, std::move(value), kValidNameMangledChars,
+ resolveValueCollision, diag);
}
-bool ResourceTable::addResourceImpl(const ResourceNameRef& name, const ResourceId resId,
- const ConfigDescription& config, std::unique_ptr<Value> value,
- const char16_t* validChars, IDiagnostics* diag) {
+bool ResourceTable::addResourceImpl(const ResourceNameRef& name,
+ const ResourceId resId,
+ const ConfigDescription& config,
+ std::unique_ptr<Value> value,
+ const char16_t* validChars,
+ std::function<int(Value*,Value*)> conflictResolver,
+ IDiagnostics* diag) {
assert(value && "value can't be nullptr");
assert(diag && "diagnostics can't be nullptr");
@@ -289,7 +304,7 @@
// This resource did not exist before, add it.
entry->values.insert(iter, ResourceConfigValue{ config, std::move(value) });
} else {
- int collisionResult = resolveValueCollision(iter->value.get(), value.get());
+ int collisionResult = conflictResolver(iter->value.get(), value.get());
if (collisionResult > 0) {
// Take the incoming value.
iter->value = std::move(value);
diff --git a/tools/aapt2/ResourceTable.h b/tools/aapt2/ResourceTable.h
index 980504b..6b7b07e 100644
--- a/tools/aapt2/ResourceTable.h
+++ b/tools/aapt2/ResourceTable.h
@@ -163,7 +163,12 @@
IDiagnostics* diag);
bool addFileReference(const ResourceNameRef& name, const ConfigDescription& config,
- const Source& source, const StringPiece16& path, IDiagnostics* diag);
+ const Source& source, const StringPiece16& path,
+ IDiagnostics* diag);
+
+ bool addFileReference(const ResourceNameRef& name, const ConfigDescription& config,
+ const Source& source, const StringPiece16& path,
+ std::function<int(Value*,Value*)> conflictResolver, IDiagnostics* diag);
/**
* Same as addResource, but doesn't verify the validity of the name. This is used
@@ -221,9 +226,14 @@
private:
ResourceTablePackage* findOrCreatePackage(const StringPiece16& name);
- bool addResourceImpl(const ResourceNameRef& name, ResourceId resId,
- const ConfigDescription& config, std::unique_ptr<Value> value,
- const char16_t* validChars, IDiagnostics* diag);
+ bool addResourceImpl(const ResourceNameRef& name,
+ ResourceId resId,
+ const ConfigDescription& config,
+ std::unique_ptr<Value> value,
+ const char16_t* validChars,
+ std::function<int(Value*,Value*)> conflictResolver,
+ IDiagnostics* diag);
+
bool setSymbolStateImpl(const ResourceNameRef& name, ResourceId resId,
const Symbol& symbol, const char16_t* validChars, IDiagnostics* diag);
};
diff --git a/tools/aapt2/link/Link.cpp b/tools/aapt2/link/Link.cpp
index 9ce3734..93f2dc6f 100644
--- a/tools/aapt2/link/Link.cpp
+++ b/tools/aapt2/link/Link.cpp
@@ -48,6 +48,7 @@
std::string outputPath;
std::string manifestPath;
std::vector<std::string> includePaths;
+ std::vector<std::string> overlayFiles;
Maybe<std::string> generateJavaClassPath;
std::vector<std::string> extraJavaPackages;
Maybe<std::string> generateProguardRulesPath;
@@ -88,9 +89,11 @@
}
};
-struct LinkCommand {
- LinkOptions mOptions;
- LinkContext mContext;
+class LinkCommand {
+public:
+ LinkCommand(const LinkOptions& options) :
+ mOptions(options), mContext(), mFinalTable() {
+ }
std::string buildResourceFileName(const ResourceFile& resFile) {
std::stringstream out;
@@ -117,8 +120,7 @@
AssetManagerSymbolTableBuilder builder;
for (const std::string& path : mOptions.includePaths) {
if (mOptions.verbose) {
- mContext.getDiagnostics()->note(
- DiagMessage(Source{ path }) << "loading include path");
+ mContext.getDiagnostics()->note(DiagMessage(path) << "loading include path");
}
std::unique_ptr<android::AssetManager> assetManager =
@@ -126,7 +128,7 @@
int32_t cookie = 0;
if (!assetManager->addAssetPath(android::String8(path.data(), path.size()), &cookie)) {
mContext.getDiagnostics()->error(
- DiagMessage(Source{ path }) << "failed to load include path");
+ DiagMessage(path) << "failed to load include path");
return {};
}
builder.add(std::move(assetManager));
@@ -141,12 +143,12 @@
std::string errorStr;
Maybe<android::FileMap> map = file::mmapPath(input, &errorStr);
if (!map) {
- mContext.getDiagnostics()->error(DiagMessage(Source{ input }) << errorStr);
+ mContext.getDiagnostics()->error(DiagMessage(input) << errorStr);
return {};
}
std::unique_ptr<ResourceTable> table = util::make_unique<ResourceTable>();
- BinaryResourceParser parser(&mContext, table.get(), Source{ input },
+ BinaryResourceParser parser(&mContext, table.get(), Source(input),
map.value().getDataPtr(), map.value().getDataLength());
if (!parser.parse()) {
return {};
@@ -160,11 +162,11 @@
std::unique_ptr<XmlResource> loadXml(const std::string& path) {
std::ifstream fin(path, std::ifstream::binary);
if (!fin) {
- mContext.getDiagnostics()->error(DiagMessage(Source{ path }) << strerror(errno));
+ mContext.getDiagnostics()->error(DiagMessage(path) << strerror(errno));
return {};
}
- return xml::inflate(&fin, mContext.getDiagnostics(), Source{ path });
+ return xml::inflate(&fin, mContext.getDiagnostics(), Source(path));
}
/**
@@ -255,9 +257,9 @@
return {};
}
- bool verifyNoExternalPackages(ResourceTable* table) {
+ bool verifyNoExternalPackages() {
bool error = false;
- for (const auto& package : table->packages) {
+ for (const auto& package : mFinalTable.packages) {
if (mContext.getCompilationPackage() != package->name ||
!package->id || package->id.value() != mContext.getPackageId()) {
// We have a package that is not related to the one we're building!
@@ -401,6 +403,103 @@
return true;
}
+ bool mergeStaticLibrary(const std::string& input) {
+ // TODO(adamlesinski): Load resources from a static library APK and merge the table into
+ // TableMerger.
+ mContext.getDiagnostics()->warn(DiagMessage()
+ << "linking static libraries not supported yet: "
+ << input);
+ return true;
+ }
+
+ bool mergeResourceTable(const std::string& input, bool override) {
+ if (mOptions.verbose) {
+ mContext.getDiagnostics()->note(DiagMessage() << "linking " << input);
+ }
+
+ std::unique_ptr<ResourceTable> table = loadTable(input);
+ if (!table) {
+ return false;
+ }
+
+ if (!mTableMerger->merge(Source(input), table.get(), override)) {
+ return false;
+ }
+ return true;
+ }
+
+ bool mergeCompiledFile(const std::string& input, ResourceFile&& file, bool override) {
+ if (file.name.package.empty()) {
+ file.name.package = mContext.getCompilationPackage().toString();
+ }
+
+ ResourceNameRef resName = file.name;
+
+ Maybe<ResourceName> mangledName = mContext.getNameMangler()->mangleName(file.name);
+ if (mangledName) {
+ resName = mangledName.value();
+ }
+
+ std::function<int(Value*,Value*)> resolver;
+ if (override) {
+ resolver = [](Value* a, Value* b) -> int {
+ int result = ResourceTable::resolveValueCollision(a, b);
+ if (result == 0) {
+ // Always accept the new value if it would normally conflict (override).
+ result = 1;
+ }
+ return result;
+ };
+ } else {
+ // Otherwise use the default resolution.
+ resolver = ResourceTable::resolveValueCollision;
+ }
+
+ // Add this file to the table.
+ if (!mFinalTable.addFileReference(resName, file.config, file.source,
+ util::utf8ToUtf16(buildResourceFileName(file)),
+ resolver, mContext.getDiagnostics())) {
+ return false;
+ }
+
+ // Add the exports of this file to the table.
+ for (SourcedResourceName& exportedSymbol : file.exportedSymbols) {
+ if (exportedSymbol.name.package.empty()) {
+ exportedSymbol.name.package = mContext.getCompilationPackage().toString();
+ }
+
+ ResourceNameRef resName = exportedSymbol.name;
+
+ Maybe<ResourceName> mangledName = mContext.getNameMangler()->mangleName(
+ exportedSymbol.name);
+ if (mangledName) {
+ resName = mangledName.value();
+ }
+
+ std::unique_ptr<Id> id = util::make_unique<Id>();
+ id->setSource(file.source.withLine(exportedSymbol.line));
+ bool result = mFinalTable.addResourceAllowMangled(resName, {}, std::move(id),
+ mContext.getDiagnostics());
+ if (!result) {
+ return false;
+ }
+ }
+
+ mFilesToProcess[resName.toResourceName()] = FileToProcess{ Source(input), std::move(file) };
+ return true;
+ }
+
+ bool processFile(const std::string& input, bool override) {
+ if (util::stringEndsWith<char>(input, ".apk")) {
+ return mergeStaticLibrary(input);
+ } else if (util::stringEndsWith<char>(input, ".arsc.flat")) {
+ return mergeResourceTable(input, override);
+ } else if (Maybe<ResourceFile> maybeF = loadFileExportHeader(input)) {
+ return mergeCompiledFile(input, std::move(maybeF.value()), override);
+ }
+ return false;
+ }
+
int run(const std::vector<std::string>& inputFiles) {
// Load the AndroidManifest.xml
std::unique_ptr<XmlResource> manifestXml = loadXml(mOptions.manifestPath);
@@ -438,82 +537,25 @@
return 1;
}
+ mTableMerger = util::make_unique<TableMerger>(&mContext, &mFinalTable);
+
if (mOptions.verbose) {
mContext.getDiagnostics()->note(
DiagMessage() << "linking package '" << mContext.mCompilationPackage << "' "
<< "with package ID " << std::hex << (int) mContext.mPackageId);
}
- ResourceTable mergedTable;
- TableMerger tableMerger(&mContext, &mergedTable);
-
- struct FilesToProcess {
- Source source;
- ResourceFile file;
- };
-
bool error = false;
- std::queue<FilesToProcess> filesToProcess;
+
for (const std::string& input : inputFiles) {
- if (util::stringEndsWith<char>(input, ".apk")) {
- // TODO(adamlesinski): Load resources from a static library APK
- // Merge the table into TableMerger.
+ if (!processFile(input, false)) {
+ error = true;
+ }
+ }
- } else if (util::stringEndsWith<char>(input, ".arsc.flat")) {
- if (mOptions.verbose) {
- mContext.getDiagnostics()->note(DiagMessage() << "linking " << input);
- }
-
- std::unique_ptr<ResourceTable> table = loadTable(input);
- if (!table) {
- return 1;
- }
-
- if (!tableMerger.merge(Source(input), table.get())) {
- return 1;
- }
-
- } else {
- // Extract the exported IDs here so we can build the resource table.
- if (Maybe<ResourceFile> maybeF = loadFileExportHeader(input)) {
- ResourceFile& f = maybeF.value();
-
- if (f.name.package.empty()) {
- f.name.package = mContext.getCompilationPackage().toString();
- }
-
- Maybe<ResourceName> mangledName = mContext.getNameMangler()->mangleName(f.name);
-
- // Add this file to the table.
- if (!mergedTable.addFileReference(mangledName ? mangledName.value() : f.name,
- f.config, f.source,
- util::utf8ToUtf16(buildResourceFileName(f)),
- mContext.getDiagnostics())) {
- error = true;
- }
-
- // Add the exports of this file to the table.
- for (SourcedResourceName& exportedSymbol : f.exportedSymbols) {
- if (exportedSymbol.name.package.empty()) {
- exportedSymbol.name.package = mContext.getCompilationPackage()
- .toString();
- }
-
- Maybe<ResourceName> mangledName = mContext.getNameMangler()->mangleName(
- exportedSymbol.name);
- std::unique_ptr<Id> id = util::make_unique<Id>();
- id->setSource(f.source.withLine(exportedSymbol.line));
- if (!mergedTable.addResourceAllowMangled(
- mangledName ? mangledName.value() : exportedSymbol.name,
- {}, std::move(id), mContext.getDiagnostics())) {
- error = true;
- }
- }
-
- filesToProcess.push(FilesToProcess{ Source(input), std::move(f) });
- } else {
- return 1;
- }
+ for (const std::string& input : mOptions.overlayFiles) {
+ if (!processFile(input, true)) {
+ error = true;
}
}
@@ -522,13 +564,13 @@
return 1;
}
- if (!verifyNoExternalPackages(&mergedTable)) {
+ if (!verifyNoExternalPackages()) {
return 1;
}
if (!mOptions.staticLib) {
PrivateAttributeMover mover;
- if (!mover.consume(&mContext, &mergedTable)) {
+ if (!mover.consume(&mContext, &mFinalTable)) {
mContext.getDiagnostics()->error(
DiagMessage() << "failed moving private attributes");
return 1;
@@ -537,22 +579,22 @@
{
IdAssigner idAssigner;
- if (!idAssigner.consume(&mContext, &mergedTable)) {
+ if (!idAssigner.consume(&mContext, &mFinalTable)) {
mContext.getDiagnostics()->error(DiagMessage() << "failed assigning IDs");
return 1;
}
}
- mContext.mNameMangler = util::make_unique<NameMangler>(
- NameManglerPolicy{ mContext.mCompilationPackage, tableMerger.getMergedPackages() });
+ mContext.mNameMangler = util::make_unique<NameMangler>(NameManglerPolicy{
+ mContext.mCompilationPackage, mTableMerger->getMergedPackages() });
mContext.mSymbols = JoinedSymbolTableBuilder()
- .addSymbolTable(util::make_unique<SymbolTableWrapper>(&mergedTable))
+ .addSymbolTable(util::make_unique<SymbolTableWrapper>(&mFinalTable))
.addSymbolTable(std::move(mContext.mSymbols))
.build();
{
ReferenceLinker linker;
- if (!linker.consume(&mContext, &mergedTable)) {
+ if (!linker.consume(&mContext, &mFinalTable)) {
mContext.getDiagnostics()->error(DiagMessage() << "failed linking references");
return 1;
}
@@ -598,20 +640,20 @@
}
}
- for (; !filesToProcess.empty(); filesToProcess.pop()) {
- FilesToProcess& f = filesToProcess.front();
- if (f.file.name.type != ResourceType::kRaw &&
- util::stringEndsWith<char>(f.source.path, ".xml.flat")) {
+ for (auto& pair : mFilesToProcess) {
+ FileToProcess& file = pair.second;
+ if (file.file.name.type != ResourceType::kRaw &&
+ util::stringEndsWith<char>(file.source.path, ".xml.flat")) {
if (mOptions.verbose) {
- mContext.getDiagnostics()->note(DiagMessage() << "linking " << f.source.path);
+ mContext.getDiagnostics()->note(DiagMessage() << "linking " << file.source.path);
}
- std::unique_ptr<XmlResource> xmlRes = loadBinaryXmlSkipFileExport(f.source.path);
+ std::unique_ptr<XmlResource> xmlRes = loadBinaryXmlSkipFileExport(file.source.path);
if (!xmlRes) {
return 1;
}
- xmlRes->file = std::move(f.file);
+ xmlRes->file = std::move(file.file);
XmlReferenceLinker xmlLinker;
if (xmlLinker.consume(&mContext, xmlRes.get())) {
@@ -631,7 +673,7 @@
}
if (!mOptions.noAutoVersion) {
- Maybe<ResourceTable::SearchResult> result = mergedTable.findResource(
+ Maybe<ResourceTable::SearchResult> result = mFinalTable.findResource(
xmlRes->file.name);
for (int sdkLevel : xmlLinker.getSdkLevels()) {
if (sdkLevel > xmlRes->file.config.sdkVersion &&
@@ -639,7 +681,7 @@
xmlRes->file.config,
sdkLevel)) {
xmlRes->file.config.sdkVersion = sdkLevel;
- if (!mergedTable.addFileReference(xmlRes->file.name,
+ if (!mFinalTable.addFileReference(xmlRes->file.name,
xmlRes->file.config,
xmlRes->file.source,
util::utf8ToUtf16(
@@ -662,10 +704,11 @@
}
} else {
if (mOptions.verbose) {
- mContext.getDiagnostics()->note(DiagMessage() << "copying " << f.source.path);
+ mContext.getDiagnostics()->note(DiagMessage() << "copying "
+ << file.source.path);
}
- if (!copyFileToArchive(f.source.path, buildResourceFileName(f.file), 0,
+ if (!copyFileToArchive(file.source.path, buildResourceFileName(file.file), 0,
archiveWriter.get())) {
error = true;
}
@@ -679,13 +722,13 @@
if (!mOptions.noAutoVersion) {
AutoVersioner versioner;
- if (!versioner.consume(&mContext, &mergedTable)) {
+ if (!versioner.consume(&mContext, &mFinalTable)) {
mContext.getDiagnostics()->error(DiagMessage() << "failed versioning styles");
return 1;
}
}
- if (!flattenTable(&mergedTable, archiveWriter.get())) {
+ if (!flattenTable(&mFinalTable, archiveWriter.get())) {
mContext.getDiagnostics()->error(DiagMessage() << "failed to write resources.arsc");
return 1;
}
@@ -704,7 +747,7 @@
// to the original package, and private and public symbols to the private package.
options.types = JavaClassGeneratorOptions::SymbolTypes::kPublic;
- if (!writeJavaFile(&mergedTable, mContext.getCompilationPackage(),
+ if (!writeJavaFile(&mFinalTable, mContext.getCompilationPackage(),
mContext.getCompilationPackage(), options)) {
return 1;
}
@@ -713,12 +756,12 @@
outputPackage = mOptions.privateSymbols.value();
}
- if (!writeJavaFile(&mergedTable, actualPackage, outputPackage, options)) {
+ if (!writeJavaFile(&mFinalTable, actualPackage, outputPackage, options)) {
return 1;
}
for (std::string& extraPackage : mOptions.extraJavaPackages) {
- if (!writeJavaFile(&mergedTable, actualPackage, util::utf8ToUtf16(extraPackage),
+ if (!writeJavaFile(&mFinalTable, actualPackage, util::utf8ToUtf16(extraPackage),
options)) {
return 1;
}
@@ -732,10 +775,10 @@
}
if (mOptions.verbose) {
- Debug::printTable(&mergedTable);
- for (; !tableMerger.getFileMergeQueue()->empty();
- tableMerger.getFileMergeQueue()->pop()) {
- const FileToMerge& f = tableMerger.getFileMergeQueue()->front();
+ Debug::printTable(&mFinalTable);
+ for (; !mTableMerger->getFileMergeQueue()->empty();
+ mTableMerger->getFileMergeQueue()->pop()) {
+ const FileToMerge& f = mTableMerger->getFileMergeQueue()->front();
mContext.getDiagnostics()->note(
DiagMessage() << f.srcPath << " -> " << f.dstPath << " from (0x"
<< std::hex << (uintptr_t) f.srcTable << std::dec);
@@ -744,6 +787,18 @@
return 0;
}
+
+private:
+ LinkOptions mOptions;
+ LinkContext mContext;
+ ResourceTable mFinalTable;
+ std::unique_ptr<TableMerger> mTableMerger;
+
+ struct FileToProcess {
+ Source source;
+ ResourceFile file;
+ };
+ std::map<ResourceName, FileToProcess> mFilesToProcess;
};
int link(const std::vector<StringPiece>& args) {
@@ -755,6 +810,9 @@
.requiredFlag("--manifest", "Path to the Android manifest to build",
&options.manifestPath)
.optionalFlagList("-I", "Adds an Android APK to link against", &options.includePaths)
+ .optionalFlagList("-R", "Compilation unit to link, using `overlay` semantics. "
+ "The last conflicting resource given takes precedence.",
+ &options.overlayFiles)
.optionalFlag("--java", "Directory in which to generate R.java",
&options.generateJavaClassPath)
.optionalFlag("--proguard", "Output file for generated Proguard rules",
@@ -794,7 +852,7 @@
options.targetSdkVersionDefault = util::utf8ToUtf16(targetSdkVersion.value());
}
- LinkCommand cmd = { options };
+ LinkCommand cmd(options);
return cmd.run(flags.getArgs());
}
diff --git a/tools/aapt2/link/TableMerger.cpp b/tools/aapt2/link/TableMerger.cpp
index 1eea410..a06a1bf 100644
--- a/tools/aapt2/link/TableMerger.cpp
+++ b/tools/aapt2/link/TableMerger.cpp
@@ -37,7 +37,7 @@
/**
* This will merge packages with the same package name (or no package name).
*/
-bool TableMerger::merge(const Source& src, ResourceTable* table) {
+bool TableMerger::merge(const Source& src, ResourceTable* table, bool overrideExisting) {
const uint8_t desiredPackageId = mContext->getPackageId();
bool error = false;
@@ -55,7 +55,7 @@
// mangled, then looked up at resolution time.
// Also, when linking, we convert references with no package name to use
// the compilation package name.
- if (!doMerge(src, table, package.get(), false)) {
+ if (!doMerge(src, table, package.get(), false, overrideExisting)) {
error = true;
}
}
@@ -79,7 +79,7 @@
bool mangle = packageName != mContext->getCompilationPackage();
mMergedPackages.insert(package->name);
- if (!doMerge(src, table, package.get(), mangle)) {
+ if (!doMerge(src, table, package.get(), mangle, false)) {
error = true;
}
}
@@ -87,7 +87,8 @@
}
bool TableMerger::doMerge(const Source& src, ResourceTable* srcTable,
- ResourceTablePackage* srcPackage, const bool manglePackage) {
+ ResourceTablePackage* srcPackage, const bool manglePackage,
+ const bool overrideExisting) {
bool error = false;
for (auto& srcType : srcPackage->types) {
@@ -149,7 +150,7 @@
if (iter != dstEntry->values.end() && iter->config == srcValue.config) {
const int collisionResult = ResourceTable::resolveValueCollision(
iter->value.get(), srcValue.value.get());
- if (collisionResult == 0) {
+ if (collisionResult == 0 && !overrideExisting) {
// Error!
ResourceNameRef resourceName(srcPackage->name,
srcType->type,
diff --git a/tools/aapt2/link/TableMerger.h b/tools/aapt2/link/TableMerger.h
index c903f1b..a2c9dbf 100644
--- a/tools/aapt2/link/TableMerger.h
+++ b/tools/aapt2/link/TableMerger.h
@@ -63,7 +63,7 @@
/**
* Merges resources from the same or empty package. This is for local sources.
*/
- bool merge(const Source& src, ResourceTable* table);
+ bool merge(const Source& src, ResourceTable* table, bool overrideExisting);
/**
* Merges resources from the given package, mangling the name. This is for static libraries.
@@ -79,7 +79,7 @@
std::queue<FileToMerge> mFilesToMerge;
bool doMerge(const Source& src, ResourceTable* srcTable, ResourceTablePackage* srcPackage,
- const bool manglePackage);
+ const bool manglePackage, const bool overrideExisting);
std::unique_ptr<Value> cloneAndMangle(ResourceTable* table, const std::u16string& package,
Value* value);
diff --git a/tools/aapt2/link/TableMerger_test.cpp b/tools/aapt2/link/TableMerger_test.cpp
index 0af4314..b7ffba7 100644
--- a/tools/aapt2/link/TableMerger_test.cpp
+++ b/tools/aapt2/link/TableMerger_test.cpp
@@ -59,7 +59,7 @@
ResourceTable finalTable;
TableMerger merger(mContext.get(), &finalTable);
- ASSERT_TRUE(merger.merge({}, tableA.get()));
+ ASSERT_TRUE(merger.merge({}, tableA.get(), false));
ASSERT_TRUE(merger.mergeAndMangle({}, u"com.app.b", tableB.get()));
EXPECT_TRUE(merger.getMergedPackages().count(u"com.app.b") != 0);
@@ -89,7 +89,7 @@
ResourceTable finalTable;
TableMerger merger(mContext.get(), &finalTable);
- ASSERT_TRUE(merger.merge({}, tableA.get()));
+ ASSERT_TRUE(merger.merge({}, tableA.get(), false));
ASSERT_TRUE(merger.mergeAndMangle({}, u"com.app.b", tableB.get()));
FileReference* f = test::getValue<FileReference>(&finalTable, u"@com.app.a:xml/file");
diff --git a/tools/layoutlib/bridge/src/android/view/BridgeInflater.java b/tools/layoutlib/bridge/src/android/view/BridgeInflater.java
index 5db1bde..723e827 100644
--- a/tools/layoutlib/bridge/src/android/view/BridgeInflater.java
+++ b/tools/layoutlib/bridge/src/android/view/BridgeInflater.java
@@ -23,6 +23,7 @@
import com.android.ide.common.rendering.api.ResourceValue;
import com.android.layoutlib.bridge.Bridge;
import com.android.layoutlib.bridge.BridgeConstants;
+import com.android.layoutlib.bridge.MockView;
import com.android.layoutlib.bridge.android.BridgeContext;
import com.android.layoutlib.bridge.android.BridgeXmlBlockParser;
import com.android.layoutlib.bridge.android.support.DrawerLayoutUtil;
@@ -126,6 +127,9 @@
if (view == null) {
view = loadCustomView(name, attrs);
}
+ } catch (InflateException e) {
+ // Don't catch the InflateException below as that results in hiding the real cause.
+ throw e;
} catch (Exception e) {
// Wrap the real exception in a ClassNotFoundException, so that the calling method
// can deal with it.
@@ -154,23 +158,30 @@
}
ta.recycle();
}
- final Object lastContext = mConstructorArgs[0];
- mConstructorArgs[0] = context;
- // try to load the class from using the custom view loader
- try {
- view = loadCustomView(name, attrs);
- } catch (Exception e2) {
- // Wrap the real exception in an InflateException so that the calling
- // method can deal with it.
- InflateException exception = new InflateException();
- if (!e2.getClass().equals(ClassNotFoundException.class)) {
- exception.initCause(e2);
- } else {
- exception.initCause(e);
+ if (!(e.getCause() instanceof ClassNotFoundException)) {
+ // There is some unknown inflation exception in inflating a View that was found.
+ view = new MockView(context, attrs);
+ ((MockView) view).setText(name);
+ Bridge.getLog().error(LayoutLog.TAG_BROKEN, e.getMessage(), e, null);
+ } else {
+ final Object lastContext = mConstructorArgs[0];
+ mConstructorArgs[0] = context;
+ // try to load the class from using the custom view loader
+ try {
+ view = loadCustomView(name, attrs);
+ } catch (Exception e2) {
+ // Wrap the real exception in an InflateException so that the calling
+ // method can deal with it.
+ InflateException exception = new InflateException();
+ if (!e2.getClass().equals(ClassNotFoundException.class)) {
+ exception.initCause(e2);
+ } else {
+ exception.initCause(e);
+ }
+ throw exception;
+ } finally {
+ mConstructorArgs[0] = lastContext;
}
- throw exception;
- } finally {
- mConstructorArgs[0] = lastContext;
}
}
diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/MockView.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/MockView.java
index 44a9aad..d392f21 100644
--- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/MockView.java
+++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/MockView.java
@@ -17,39 +17,90 @@
package com.android.layoutlib.bridge;
import android.content.Context;
-import android.graphics.Canvas;
import android.util.AttributeSet;
import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
import android.widget.TextView;
/**
* Base class for mocked views.
- *
- * TODO: implement onDraw and draw a rectangle in a random color with the name of the class
- * (or better the id of the view).
+ * <p/>
+ * FrameLayout with a single TextView. Doesn't allow adding any other views to itself.
*/
-public class MockView extends TextView {
+public class MockView extends FrameLayout {
+
+ private final TextView mView;
+
+ public MockView(Context context) {
+ this(context, null);
+ }
public MockView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
- public MockView(Context context, AttributeSet attrs, int defStyle) {
- this(context, attrs, defStyle, 0);
+ public MockView(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
}
public MockView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
-
- setText(this.getClass().getSimpleName());
- setTextColor(0xFF000000);
+ mView = new TextView(context, attrs);
+ mView.setTextColor(0xFF000000);
setGravity(Gravity.CENTER);
+ setText(getClass().getSimpleName());
+ addView(mView);
+ setBackgroundColor(0xFF7F7F7F);
+ }
+
+ // Only allow adding one TextView.
+ @Override
+ public void addView(View child) {
+ if (child == mView) {
+ super.addView(child);
+ }
}
@Override
- public void onDraw(Canvas canvas) {
- canvas.drawARGB(0xFF, 0x7F, 0x7F, 0x7F);
+ public void addView(View child, int index) {
+ if (child == mView) {
+ super.addView(child, index);
+ }
+ }
- super.onDraw(canvas);
+ @Override
+ public void addView(View child, int width, int height) {
+ if (child == mView) {
+ super.addView(child, width, height);
+ }
+ }
+
+ @Override
+ public void addView(View child, ViewGroup.LayoutParams params) {
+ if (child == mView) {
+ super.addView(child, params);
+ }
+ }
+
+ @Override
+ public void addView(View child, int index, ViewGroup.LayoutParams params) {
+ if (child == mView) {
+ super.addView(child, index, params);
+ }
+ }
+
+ // The following methods are called by the IDE via reflection, and should be considered part
+ // of the API.
+ // Historically, MockView used to be a textView and had these methods. Now, we simply delegate
+ // them to the contained textView.
+
+ public void setText(CharSequence text) {
+ mView.setText(text);
+ }
+
+ public void setGravity(int gravity) {
+ mView.setGravity(gravity);
}
}