Merge "Moved system user apps whitelisting to PM"
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/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/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/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/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/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/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/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/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/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 cdcf79d..c152514 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -12809,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/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index f17698c..a22f821 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -7069,15 +7069,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) {}
}
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..decfb34 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.
@@ -183,6 +181,8 @@
int mAttrType;
+ private final Rect mTmpSize = new Rect();
+
WindowStateAnimator(final WindowState win) {
final WindowManagerService service = win.mService;
@@ -442,7 +442,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();
}
}
@@ -581,52 +581,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 +656,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 +673,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 +906,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 +989,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) {
@@ -1157,65 +1172,12 @@
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);
@@ -1532,7 +1494,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/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);
}
}