diff options
165 files changed, 5416 insertions, 1824 deletions
diff --git a/Android.mk b/Android.mk index 4cedadcb7764..6fb5aec2eccc 100644 --- a/Android.mk +++ b/Android.mk @@ -292,6 +292,7 @@ LOCAL_SRC_FILES += \ core/java/android/service/euicc/IGetEidCallback.aidl \ core/java/android/service/euicc/IGetEuiccInfoCallback.aidl \ core/java/android/service/euicc/IGetEuiccProfileInfoListCallback.aidl \ + core/java/android/service/euicc/IRetainSubscriptionsForFactoryResetCallback.aidl \ core/java/android/service/euicc/ISwitchToSubscriptionCallback.aidl \ core/java/android/service/euicc/IUpdateSubscriptionNicknameCallback.aidl \ core/java/android/service/gatekeeper/IGateKeeperService.aidl \ @@ -437,10 +438,6 @@ LOCAL_SRC_FILES += \ location/java/android/location/INetInitiatedListener.aidl \ location/java/com/android/internal/location/ILocationProvider.aidl \ media/java/android/media/IAudioService.aidl \ - ../av/drm/libmediadrm/aidl/android/media/ICas.aidl \ - ../av/drm/libmediadrm/aidl/android/media/ICasListener.aidl \ - ../av/drm/libmediadrm/aidl/android/media/IDescrambler.aidl \ - ../av/drm/libmediadrm/aidl/android/media/IMediaCasService.aidl \ media/java/android/media/IAudioFocusDispatcher.aidl \ media/java/android/media/IAudioRoutesObserver.aidl \ media/java/android/media/IMediaHTTPConnection.aidl \ @@ -613,6 +610,8 @@ LOCAL_STATIC_JAVA_LIBRARIES := \ android.hardware.vibrator-V1.1-java-constants \ android.hardware.wifi-V1.0-java-constants \ +include hardware/interfaces/cas/1.0/CasHal.mk + # Loaded with System.loadLibrary by android.view.textclassifier LOCAL_REQUIRED_MODULES += libtextclassifier diff --git a/apct-tests/perftests/core/src/android/os/ParcelPerfTest.java b/apct-tests/perftests/core/src/android/os/ParcelPerfTest.java index 8cd45f7f92bd..a92597f131c3 100644 --- a/apct-tests/perftests/core/src/android/os/ParcelPerfTest.java +++ b/apct-tests/perftests/core/src/android/os/ParcelPerfTest.java @@ -57,6 +57,46 @@ public class ParcelPerfTest { } @Test + public void timeGetDataPosition() { + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + while (state.keepRunning()) { + mParcel.dataPosition(); + } + } + + @Test + public void timeSetDataSize() { + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + while (state.keepRunning()) { + mParcel.setDataSize(0); + } + } + + @Test + public void timeGetDataSize() { + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + while (state.keepRunning()) { + mParcel.dataSize(); + } + } + + @Test + public void timeSetDataCapacity() { + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + while (state.keepRunning()) { + mParcel.setDataCapacity(0); + } + } + + @Test + public void timeGetDataCapacity() { + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + while (state.keepRunning()) { + mParcel.dataCapacity(); + } + } + + @Test public void timeWriteByte() { final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); final byte val = 0xF; @@ -112,4 +152,19 @@ public class ParcelPerfTest { mParcel.readLong(); } } + + @Test + public void timeObtainRecycle() { + // Use up the pooled instances. + // A lot bigger than the actual size but in case someone increased it. + final int POOL_SIZE = 100; + for (int i = 0; i < POOL_SIZE; i++) { + Parcel.obtain(); + } + + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + while (state.keepRunning()) { + Parcel.obtain().recycle(); + } + } } diff --git a/api/current.txt b/api/current.txt index 0eab1fd802ca..17dbcbf392b9 100644 --- a/api/current.txt +++ b/api/current.txt @@ -36908,6 +36908,30 @@ package android.service.autofill { field public static final java.lang.String SERVICE_META_DATA = "android.autofill"; } + public final class CharSequenceTransformation implements android.os.Parcelable { + method public int describeContents(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.service.autofill.CharSequenceTransformation> CREATOR; + } + + public static class CharSequenceTransformation.Builder { + ctor public CharSequenceTransformation.Builder(); + method public android.service.autofill.CharSequenceTransformation.Builder addField(android.view.autofill.AutofillId, java.lang.String, java.lang.String); + method public android.service.autofill.CharSequenceTransformation build(); + } + + public final class CustomDescription implements android.os.Parcelable { + method public int describeContents(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.service.autofill.CustomDescription> CREATOR; + } + + public static class CustomDescription.Builder { + ctor public CustomDescription.Builder(android.widget.RemoteViews); + method public android.service.autofill.CustomDescription.Builder addChild(int, android.service.autofill.Transformation); + method public android.service.autofill.CustomDescription build(); + } + public final class Dataset implements android.os.Parcelable { method public int describeContents(); method public void writeToParcel(android.os.Parcel, int); @@ -36981,6 +37005,25 @@ package android.service.autofill { method public android.service.autofill.FillResponse.Builder setSaveInfo(android.service.autofill.SaveInfo); } + public final class ImageTransformation implements android.os.Parcelable { + method public int describeContents(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.service.autofill.ImageTransformation> CREATOR; + } + + public static class ImageTransformation.Builder { + ctor public ImageTransformation.Builder(android.view.autofill.AutofillId); + method public android.service.autofill.ImageTransformation.Builder addOption(java.lang.String, int); + method public android.service.autofill.ImageTransformation build(); + } + + public final class LuhnChecksumValidator implements android.os.Parcelable { + ctor public LuhnChecksumValidator(android.view.autofill.AutofillId...); + method public int describeContents(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.service.autofill.LuhnChecksumValidator> CREATOR; + } + public final class SaveCallback { method public void onFailure(java.lang.CharSequence); method public void onSuccess(); @@ -37004,10 +37047,12 @@ package android.service.autofill { public static final class SaveInfo.Builder { ctor public SaveInfo.Builder(int, android.view.autofill.AutofillId[]); method public android.service.autofill.SaveInfo build(); + method public android.service.autofill.SaveInfo.Builder setCustomDescription(android.service.autofill.CustomDescription); method public android.service.autofill.SaveInfo.Builder setDescription(java.lang.CharSequence); method public android.service.autofill.SaveInfo.Builder setFlags(int); method public android.service.autofill.SaveInfo.Builder setNegativeAction(int, android.content.IntentSender); method public android.service.autofill.SaveInfo.Builder setOptionalIds(android.view.autofill.AutofillId[]); + method public android.service.autofill.SaveInfo.Builder setValidator(android.service.autofill.Validator); } public final class SaveRequest implements android.os.Parcelable { @@ -37018,6 +37063,24 @@ package android.service.autofill { field public static final android.os.Parcelable.Creator<android.service.autofill.SaveRequest> CREATOR; } + public final class SimpleRegexValidator implements android.os.Parcelable { + ctor public SimpleRegexValidator(android.view.autofill.AutofillId, java.lang.String); + method public int describeContents(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.service.autofill.SimpleRegexValidator> CREATOR; + } + + public abstract interface Transformation { + } + + public abstract interface Validator { + } + + public final class Validators { + method public static android.service.autofill.Validator and(android.service.autofill.Validator...); + method public static android.service.autofill.Validator or(android.service.autofill.Validator...); + } + } package android.service.carrier { diff --git a/api/system-current.txt b/api/system-current.txt index 833e1f6fc7c1..53e1b81d95d3 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -39985,6 +39985,30 @@ package android.service.autofill { field public static final java.lang.String SERVICE_META_DATA = "android.autofill"; } + public final class CharSequenceTransformation implements android.os.Parcelable { + method public int describeContents(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.service.autofill.CharSequenceTransformation> CREATOR; + } + + public static class CharSequenceTransformation.Builder { + ctor public CharSequenceTransformation.Builder(); + method public android.service.autofill.CharSequenceTransformation.Builder addField(android.view.autofill.AutofillId, java.lang.String, java.lang.String); + method public android.service.autofill.CharSequenceTransformation build(); + } + + public final class CustomDescription implements android.os.Parcelable { + method public int describeContents(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.service.autofill.CustomDescription> CREATOR; + } + + public static class CustomDescription.Builder { + ctor public CustomDescription.Builder(android.widget.RemoteViews); + method public android.service.autofill.CustomDescription.Builder addChild(int, android.service.autofill.Transformation); + method public android.service.autofill.CustomDescription build(); + } + public final class Dataset implements android.os.Parcelable { method public int describeContents(); method public void writeToParcel(android.os.Parcel, int); @@ -40058,6 +40082,25 @@ package android.service.autofill { method public android.service.autofill.FillResponse.Builder setSaveInfo(android.service.autofill.SaveInfo); } + public final class ImageTransformation implements android.os.Parcelable { + method public int describeContents(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.service.autofill.ImageTransformation> CREATOR; + } + + public static class ImageTransformation.Builder { + ctor public ImageTransformation.Builder(android.view.autofill.AutofillId); + method public android.service.autofill.ImageTransformation.Builder addOption(java.lang.String, int); + method public android.service.autofill.ImageTransformation build(); + } + + public final class LuhnChecksumValidator implements android.os.Parcelable { + ctor public LuhnChecksumValidator(android.view.autofill.AutofillId...); + method public int describeContents(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.service.autofill.LuhnChecksumValidator> CREATOR; + } + public final class SaveCallback { method public void onFailure(java.lang.CharSequence); method public void onSuccess(); @@ -40081,10 +40124,12 @@ package android.service.autofill { public static final class SaveInfo.Builder { ctor public SaveInfo.Builder(int, android.view.autofill.AutofillId[]); method public android.service.autofill.SaveInfo build(); + method public android.service.autofill.SaveInfo.Builder setCustomDescription(android.service.autofill.CustomDescription); method public android.service.autofill.SaveInfo.Builder setDescription(java.lang.CharSequence); method public android.service.autofill.SaveInfo.Builder setFlags(int); method public android.service.autofill.SaveInfo.Builder setNegativeAction(int, android.content.IntentSender); method public android.service.autofill.SaveInfo.Builder setOptionalIds(android.view.autofill.AutofillId[]); + method public android.service.autofill.SaveInfo.Builder setValidator(android.service.autofill.Validator); } public final class SaveRequest implements android.os.Parcelable { @@ -40095,6 +40140,24 @@ package android.service.autofill { field public static final android.os.Parcelable.Creator<android.service.autofill.SaveRequest> CREATOR; } + public final class SimpleRegexValidator implements android.os.Parcelable { + ctor public SimpleRegexValidator(android.view.autofill.AutofillId, java.lang.String); + method public int describeContents(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.service.autofill.SimpleRegexValidator> CREATOR; + } + + public abstract interface Transformation { + } + + public abstract interface Validator { + } + + public final class Validators { + method public static android.service.autofill.Validator and(android.service.autofill.Validator...); + method public static android.service.autofill.Validator or(android.service.autofill.Validator...); + } + } package android.service.carrier { diff --git a/api/test-current.txt b/api/test-current.txt index e6c22ae979f0..abfcaba3727e 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -37080,6 +37080,30 @@ package android.service.autofill { field public static final java.lang.String SERVICE_META_DATA = "android.autofill"; } + public final class CharSequenceTransformation implements android.os.Parcelable { + method public int describeContents(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.service.autofill.CharSequenceTransformation> CREATOR; + } + + public static class CharSequenceTransformation.Builder { + ctor public CharSequenceTransformation.Builder(); + method public android.service.autofill.CharSequenceTransformation.Builder addField(android.view.autofill.AutofillId, java.lang.String, java.lang.String); + method public android.service.autofill.CharSequenceTransformation build(); + } + + public final class CustomDescription implements android.os.Parcelable { + method public int describeContents(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.service.autofill.CustomDescription> CREATOR; + } + + public static class CustomDescription.Builder { + ctor public CustomDescription.Builder(android.widget.RemoteViews); + method public android.service.autofill.CustomDescription.Builder addChild(int, android.service.autofill.Transformation); + method public android.service.autofill.CustomDescription build(); + } + public final class Dataset implements android.os.Parcelable { method public int describeContents(); method public void writeToParcel(android.os.Parcel, int); @@ -37153,6 +37177,25 @@ package android.service.autofill { method public android.service.autofill.FillResponse.Builder setSaveInfo(android.service.autofill.SaveInfo); } + public final class ImageTransformation implements android.os.Parcelable { + method public int describeContents(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.service.autofill.ImageTransformation> CREATOR; + } + + public static class ImageTransformation.Builder { + ctor public ImageTransformation.Builder(android.view.autofill.AutofillId); + method public android.service.autofill.ImageTransformation.Builder addOption(java.lang.String, int); + method public android.service.autofill.ImageTransformation build(); + } + + public final class LuhnChecksumValidator implements android.os.Parcelable { + ctor public LuhnChecksumValidator(android.view.autofill.AutofillId...); + method public int describeContents(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.service.autofill.LuhnChecksumValidator> CREATOR; + } + public final class SaveCallback { method public void onFailure(java.lang.CharSequence); method public void onSuccess(); @@ -37176,10 +37219,12 @@ package android.service.autofill { public static final class SaveInfo.Builder { ctor public SaveInfo.Builder(int, android.view.autofill.AutofillId[]); method public android.service.autofill.SaveInfo build(); + method public android.service.autofill.SaveInfo.Builder setCustomDescription(android.service.autofill.CustomDescription); method public android.service.autofill.SaveInfo.Builder setDescription(java.lang.CharSequence); method public android.service.autofill.SaveInfo.Builder setFlags(int); method public android.service.autofill.SaveInfo.Builder setNegativeAction(int, android.content.IntentSender); method public android.service.autofill.SaveInfo.Builder setOptionalIds(android.view.autofill.AutofillId[]); + method public android.service.autofill.SaveInfo.Builder setValidator(android.service.autofill.Validator); } public final class SaveRequest implements android.os.Parcelable { @@ -37190,6 +37235,24 @@ package android.service.autofill { field public static final android.os.Parcelable.Creator<android.service.autofill.SaveRequest> CREATOR; } + public final class SimpleRegexValidator implements android.os.Parcelable { + ctor public SimpleRegexValidator(android.view.autofill.AutofillId, java.lang.String); + method public int describeContents(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.service.autofill.SimpleRegexValidator> CREATOR; + } + + public abstract interface Transformation { + } + + public abstract interface Validator { + } + + public final class Validators { + method public static android.service.autofill.Validator and(android.service.autofill.Validator...); + method public static android.service.autofill.Validator or(android.service.autofill.Validator...); + } + } package android.service.carrier { diff --git a/cmds/am/src/com/android/commands/am/Instrument.java b/cmds/am/src/com/android/commands/am/Instrument.java index 4966b4380d9b..c6d83f51a40a 100644 --- a/cmds/am/src/com/android/commands/am/Instrument.java +++ b/cmds/am/src/com/android/commands/am/Instrument.java @@ -25,7 +25,6 @@ import android.content.pm.IPackageManager; import android.content.pm.InstrumentationInfo; import android.os.Build; import android.os.Bundle; -import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserHandle; import android.util.AndroidException; @@ -34,6 +33,8 @@ import android.view.IWindowManager; import java.io.IOException; import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.List; @@ -95,6 +96,12 @@ public class Instrument { public void onError(String errorText, boolean commandError); } + private static Collection<String> sorted(Collection<String> list) { + final ArrayList<String> copy = new ArrayList<>(list); + Collections.sort(copy); + return copy; + } + /** * Printer for the 'classic' text based status reporting. */ @@ -124,7 +131,7 @@ public class Instrument { System.out.print(pretty); } else { if (results != null) { - for (String key : results.keySet()) { + for (String key : sorted(results.keySet())) { System.out.println( "INSTRUMENTATION_STATUS: " + key + "=" + results.get(key)); } @@ -214,7 +221,7 @@ public class Instrument { private void writeBundle(ProtoOutputStream proto, long fieldId, Bundle bundle) { final long bundleToken = proto.startObject(fieldId); - for (final String key: bundle.keySet()) { + for (final String key: sorted(bundle.keySet())) { final long entryToken = proto.startRepeatedObject( InstrumentationData.ResultsBundle.ENTRIES); diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index bc6e9cd0ab7e..0ff3215e1271 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -6416,17 +6416,7 @@ public class Activity extends ContextThemeWrapper */ @Deprecated public boolean requestVisibleBehind(boolean visible) { - if (!mResumed) { - // Do not permit paused or stopped activities to do this. - visible = false; - } - try { - mVisibleBehind = ActivityManager.getService() - .requestVisibleBehind(mToken, visible) && visible; - } catch (RemoteException e) { - mVisibleBehind = false; - } - return mVisibleBehind; + return false; } /** diff --git a/core/java/android/app/admin/DeviceAdminReceiver.java b/core/java/android/app/admin/DeviceAdminReceiver.java index d9b6eed4fc4d..5a356d9d4a6d 100644 --- a/core/java/android/app/admin/DeviceAdminReceiver.java +++ b/core/java/android/app/admin/DeviceAdminReceiver.java @@ -238,8 +238,7 @@ public class DeviceAdminReceiver extends BroadcastReceiver { * {@link android.app.admin.DevicePolicyManager#isProfileOwnerApp}. You will generally handle * this in {@link DeviceAdminReceiver#onProfileProvisioningComplete}. * - * <p>Input: Nothing.</p> - * <p>Output: Nothing</p> + * @see DevicePolicyManager#ACTION_PROVISIONING_SUCCESSFUL */ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) @BroadcastBehavior(explicitOnly = true) @@ -669,6 +668,11 @@ public class DeviceAdminReceiver extends BroadcastReceiver { * profile owner needs to wait for data to be available if required (e.g. android device ids or * other data that is set as a result of server interactions). * + * <p>From version {@link android.os.Build.VERSION_CODES#O}, when managed provisioning has + * completed, along with this callback the activity intent + * {@link DevicePolicyManager#ACTION_PROVISIONING_SUCCESSFUL} will also be sent to the same + * application. + * * @param context The running context as per {@link #onReceive}. * @param intent The received intent as per {@link #onReceive}. */ diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index 14162afdb25d..f612eaa43578 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -169,8 +169,7 @@ public class DevicePolicyManager { * * <p>From version {@link android.os.Build.VERSION_CODES#O}, when managed provisioning has * completed, along with the above broadcast, activity intent - * {@link #ACTION_PROVISIONING_SUCCESSFUL} will also be sent to the application specified in - * the provisioning intent. + * {@link #ACTION_PROVISIONING_SUCCESSFUL} will also be sent to the profile owner. * * <p>If provisioning fails, the managedProfile is removed so the device returns to its * previous state. @@ -857,8 +856,7 @@ public class DevicePolicyManager { * {@link DeviceAdminReceiver#ACTION_PROFILE_PROVISIONING_COMPLETE} broadcast but this will be * delivered faster as it's an activity intent. * - * <p>The intent is only sent to the application on the profile that requested provisioning. In - * the device owner case the profile is the primary user. + * <p>The intent is only sent to the new device or profile owner. * * @see #ACTION_PROVISION_MANAGED_PROFILE * @see #ACTION_PROVISION_MANAGED_DEVICE diff --git a/core/java/android/content/pm/permission/IRuntimePermissionPresenter.aidl b/core/java/android/content/pm/permission/IRuntimePermissionPresenter.aidl index 3c3b84d7b2a3..9490e276f228 100644 --- a/core/java/android/content/pm/permission/IRuntimePermissionPresenter.aidl +++ b/core/java/android/content/pm/permission/IRuntimePermissionPresenter.aidl @@ -25,4 +25,5 @@ import android.os.RemoteCallback; */ oneway interface IRuntimePermissionPresenter { void getAppPermissions(String packageName, in RemoteCallback callback); + void revokeRuntimePermission(String packageName, String permissionName); } diff --git a/core/java/android/content/pm/permission/RuntimePermissionPresenter.java b/core/java/android/content/pm/permission/RuntimePermissionPresenter.java index 6d55d2f7a860..02d0a6d8bd36 100644 --- a/core/java/android/content/pm/permission/RuntimePermissionPresenter.java +++ b/core/java/android/content/pm/permission/RuntimePermissionPresenter.java @@ -22,7 +22,6 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; -import android.content.pm.ApplicationInfo; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; @@ -31,6 +30,7 @@ import android.os.RemoteCallback; import android.os.RemoteException; import android.permissionpresenterservice.RuntimePermissionPresenterService; import android.util.Log; + import com.android.internal.annotations.GuardedBy; import com.android.internal.os.SomeArgs; @@ -118,6 +118,22 @@ public final class RuntimePermissionPresenter { mRemoteService.processMessage(message); } + /** + * Revoke the permission {@code permissionName} for app {@code packageName} + * + * @param packageName The package for which to revoke + * @param permissionName The permission to revoke + */ + public void revokeRuntimePermission(String packageName, String permissionName) { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = packageName; + args.arg2 = permissionName; + + Message message = mRemoteService.obtainMessage( + RemoteService.MSG_REVOKE_APP_PERMISSIONS, args); + mRemoteService.processMessage(message); + } + private static final class RemoteService extends Handler implements ServiceConnection { private static final long UNBIND_TIMEOUT_MILLIS = 10000; @@ -125,6 +141,7 @@ public final class RuntimePermissionPresenter { public static final int MSG_GET_APP_PERMISSIONS = 1; public static final int MSG_GET_APPS_USING_PERMISSIONS = 2; public static final int MSG_UNBIND = 3; + public static final int MSG_REVOKE_APP_PERMISSIONS = 4; private final Object mLock = new Object(); @@ -231,6 +248,25 @@ public final class RuntimePermissionPresenter { mRemoteInstance = null; } } break; + + case MSG_REVOKE_APP_PERMISSIONS: { + SomeArgs args = (SomeArgs) msg.obj; + final String packageName = (String) args.arg1; + final String permissionName = (String) args.arg2; + args.recycle(); + final IRuntimePermissionPresenter remoteInstance; + synchronized (mLock) { + remoteInstance = mRemoteInstance; + } + if (remoteInstance == null) { + return; + } + try { + remoteInstance.revokeRuntimePermission(packageName, permissionName); + } catch (RemoteException re) { + Log.e(TAG, "Error getting app permissions", re); + } + } break; } synchronized (mLock) { diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java index 68a81ca341a7..801c002fb568 100644 --- a/core/java/android/os/Parcel.java +++ b/core/java/android/os/Parcel.java @@ -27,6 +27,7 @@ import android.util.SparseArray; import android.util.SparseBooleanArray; import android.util.SparseIntArray; +import dalvik.annotation.optimization.CriticalNative; import dalvik.annotation.optimization.FastNative; import dalvik.system.VMRuntime; @@ -260,24 +261,24 @@ public final class Parcel { // see libbinder's binder/Status.h private static final int EX_TRANSACTION_FAILED = -129; - @FastNative + @CriticalNative private static native int nativeDataSize(long nativePtr); - @FastNative + @CriticalNative private static native int nativeDataAvail(long nativePtr); - @FastNative + @CriticalNative private static native int nativeDataPosition(long nativePtr); - @FastNative + @CriticalNative private static native int nativeDataCapacity(long nativePtr); @FastNative private static native long nativeSetDataSize(long nativePtr, int size); - @FastNative + @CriticalNative private static native void nativeSetDataPosition(long nativePtr, int pos); @FastNative private static native void nativeSetDataCapacity(long nativePtr, int size); - @FastNative + @CriticalNative private static native boolean nativePushAllowFds(long nativePtr, boolean allowFds); - @FastNative + @CriticalNative private static native void nativeRestoreAllowFds(long nativePtr, boolean lastValue); private static native void nativeWriteByteArray(long nativePtr, byte[] b, int offset, int len); @@ -297,13 +298,13 @@ public final class Parcel { private static native byte[] nativeCreateByteArray(long nativePtr); private static native boolean nativeReadByteArray(long nativePtr, byte[] dest, int destLen); private static native byte[] nativeReadBlob(long nativePtr); - @FastNative + @CriticalNative private static native int nativeReadInt(long nativePtr); - @FastNative + @CriticalNative private static native long nativeReadLong(long nativePtr); - @FastNative + @CriticalNative private static native float nativeReadFloat(long nativePtr); - @FastNative + @CriticalNative private static native double nativeReadDouble(long nativePtr); private static native String nativeReadString(long nativePtr); private static native IBinder nativeReadStrongBinder(long nativePtr); @@ -319,11 +320,12 @@ public final class Parcel { private static native int nativeCompareData(long thisNativePtr, long otherNativePtr); private static native long nativeAppendFrom( long thisNativePtr, long otherNativePtr, int offset, int length); - @FastNative + @CriticalNative private static native boolean nativeHasFileDescriptors(long nativePtr); private static native void nativeWriteInterfaceToken(long nativePtr, String interfaceName); private static native void nativeEnforceInterface(long nativePtr, String interfaceName); + @CriticalNative private static native long nativeGetBlobAshmemSize(long nativePtr); public final static Parcelable.Creator<String> STRING_CREATOR diff --git a/core/java/android/permissionpresenterservice/RuntimePermissionPresenterService.java b/core/java/android/permissionpresenterservice/RuntimePermissionPresenterService.java index 344d947a3ad3..2931627f0ec0 100644 --- a/core/java/android/permissionpresenterservice/RuntimePermissionPresenterService.java +++ b/core/java/android/permissionpresenterservice/RuntimePermissionPresenterService.java @@ -20,7 +20,6 @@ import android.annotation.SystemApi; import android.app.Service; import android.content.Context; import android.content.Intent; -import android.content.pm.ApplicationInfo; import android.content.pm.permission.IRuntimePermissionPresenter; import android.content.pm.permission.RuntimePermissionPresentationInfo; import android.content.pm.permission.RuntimePermissionPresenter; @@ -30,6 +29,7 @@ import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.os.RemoteCallback; + import com.android.internal.os.SomeArgs; import java.util.List; @@ -72,6 +72,16 @@ public abstract class RuntimePermissionPresenterService extends Service { */ public abstract List<RuntimePermissionPresentationInfo> onGetAppPermissions(String packageName); + /** + * Revoke the permission {@code permissionName} for app {@code packageName} + * + * @param packageName The package for which to revoke + * @param permissionName The permission to revoke + * + * @hide + */ + public abstract void onRevokeRuntimePermission(String packageName, String permissionName); + @Override public final IBinder onBind(Intent intent) { return new IRuntimePermissionPresenter.Stub() { @@ -83,12 +93,22 @@ public abstract class RuntimePermissionPresenterService extends Service { mHandler.obtainMessage(MyHandler.MSG_GET_APP_PERMISSIONS, args).sendToTarget(); } + + @Override + public void revokeRuntimePermission(String packageName, String permissionName) { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = packageName; + args.arg2 = permissionName; + mHandler.obtainMessage(MyHandler.MSG_REVOKE_APP_PERMISSION, + args).sendToTarget(); + } }; } private final class MyHandler extends Handler { public static final int MSG_GET_APP_PERMISSIONS = 1; public static final int MSG_GET_APPS_USING_PERMISSIONS = 2; + public static final int MSG_REVOKE_APP_PERMISSION = 3; public MyHandler(Looper looper) { super(looper, null, false); @@ -113,6 +133,14 @@ public abstract class RuntimePermissionPresenterService extends Service { callback.sendResult(null); } } break; + case MSG_REVOKE_APP_PERMISSION: { + SomeArgs args = (SomeArgs) msg.obj; + String packageName = (String) args.arg1; + String permissionName = (String) args.arg2; + args.recycle(); + + onRevokeRuntimePermission(packageName, permissionName); + } break; } } } diff --git a/core/java/android/service/autofill/CharSequenceTransformation.java b/core/java/android/service/autofill/CharSequenceTransformation.java new file mode 100644 index 000000000000..7472aba99c21 --- /dev/null +++ b/core/java/android/service/autofill/CharSequenceTransformation.java @@ -0,0 +1,202 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.autofill; + +import static android.view.autofill.Helper.sDebug; + +import android.annotation.NonNull; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.ArrayMap; +import android.util.Log; +import android.util.Pair; +import android.view.autofill.AutofillId; +import android.widget.RemoteViews; +import android.widget.TextView; + +import com.android.internal.util.Preconditions; + +/** + * Replaces a {@link TextView} child of a {@link CustomDescription} with the contents of one or + * more regular expressions (regexs). + * + * <p>When it contains more than one field, the fields that match their regex are added to the + * overall transformation result. + * + * <p>For example, a transformation to mask a credit card number contained in just one field would + * be: + * + * <pre class="prettyprint"> + * new CharSequenceTransformation.Builder() + * .addField(ccNumberId, "^.*(\\d\\d\\d\\d)$", "...$1") + * .build(); + * </pre> + * + * <p>But a tranformation that generates a {@code Exp: MM / YYYY} credit expiration date from two + * fields (month and year) would be: + * + * <pre class="prettyprint"> + * new CharSequenceTransformation.Builder() + * .addField(ccExpMonthId, "^(\\d\\d)$", "Exp: $1") + * .addField(ccExpYearId, "^(\\d\\d\\d\\d)$", " / $1"); + * </pre> + */ +//TODO(b/62534917): add unit tests +public final class CharSequenceTransformation extends InternalTransformation implements Parcelable { + private static final String TAG = "CharSequenceTransformation"; + private final ArrayMap<AutofillId, Pair<String, String>> mFields; + + private CharSequenceTransformation(Builder builder) { + mFields = builder.mFields; + } + + /** @hide */ + @Override + public void apply(@NonNull ValueFinder finder, @NonNull RemoteViews parentTemplate, + int childViewId) { + final StringBuilder converted = new StringBuilder(); + final int size = mFields.size(); + if (sDebug) Log.d(TAG, size + " multiple fields on id " + childViewId); + for (int i = 0; i < size; i++) { + final AutofillId id = mFields.keyAt(i); + final Pair<String, String> regex = mFields.valueAt(i); + final String value = finder.findByAutofillId(id); + if (value == null) { + Log.w(TAG, "No value for id " + id); + return; + } + final String convertedValue = value.replaceAll(regex.first, regex.second); + converted.append(convertedValue); + } + parentTemplate.setCharSequence(childViewId, "setText", converted); + } + + /** + * Builder for {@link CharSequenceTransformation} objects. + */ + public static class Builder { + private ArrayMap<AutofillId, Pair<String, String>> mFields; + private boolean mDestroyed; + + //TODO(b/62534917): add constructor that takes a field so we force it to have at least one + // (and then remove the check for empty from build()) + + /** + * Adds the transformed contents of a field to the overall result of this transformation. + * + * @param id id of the screen field. + * @param regex regular expression with groups (delimited by {@code (} and {@code (}) that + * are used to substitute parts of the value. + * @param subst the string that substitutes the matched regex, using {@code $} for + * group substitution ({@code $1} for 1st group match, {@code $2} for 2nd, etc). + * + * @return this builder. + */ + public Builder addField(@NonNull AutofillId id, @NonNull String regex, + @NonNull String subst) { + //TODO(b/62534917): throw exception if regex /subts are invalid + throwIfDestroyed(); + Preconditions.checkNotNull(id); + Preconditions.checkNotNull(regex); + Preconditions.checkNotNull(subst); + if (mFields == null) { + mFields = new ArrayMap<>(); + } + mFields.put(id, new Pair<>(regex, subst)); + return this; + } + + /** + * Creates a new {@link CharSequenceTransformation} instance. + * + * @throws IllegalStateException if no call to {@link #addField(AutofillId, String, String)} + * was made. + */ + public CharSequenceTransformation build() { + throwIfDestroyed(); + Preconditions.checkState(mFields != null && !mFields.isEmpty(), + "Must add at least one field"); + mDestroyed = true; + return new CharSequenceTransformation(this); + } + + private void throwIfDestroyed() { + Preconditions.checkState(!mDestroyed, "Already called build()"); + } + } + + ///////////////////////////////////// + // Object "contract" methods. // + ///////////////////////////////////// + @Override + public String toString() { + if (!sDebug) return super.toString(); + + return "MultipleViewsCharSequenceTransformation: [fields=" + mFields + "]"; + } + + ///////////////////////////////////// + // Parcelable "contract" methods. // + ///////////////////////////////////// + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int flags) { + final int size = mFields.size(); + final AutofillId[] ids = new AutofillId[size]; + final String[] regexs = new String[size]; + final String[] substs = new String[size]; + Pair<String, String> pair; + for (int i = 0; i < size; i++) { + ids[i] = mFields.keyAt(i); + pair = mFields.valueAt(i); + regexs[i] = pair.first; + substs[i] = pair.second; + } + parcel.writeParcelableArray(ids, flags); + parcel.writeStringArray(regexs); + parcel.writeStringArray(substs); + } + + public static final Parcelable.Creator<CharSequenceTransformation> CREATOR = + new Parcelable.Creator<CharSequenceTransformation>() { + @Override + public CharSequenceTransformation createFromParcel(Parcel parcel) { + // Always go through the builder to ensure the data ingested by + // the system obeys the contract of the builder to avoid attacks + // using specially crafted parcels. + final CharSequenceTransformation.Builder builder = + new CharSequenceTransformation.Builder(); + final AutofillId[] ids = parcel.readParcelableArray(null, AutofillId.class); + final String[] regexs = parcel.createStringArray(); + final String[] substs = parcel.createStringArray(); + final int size = ids.length; + for (int i = 0; i < size; i++) { + builder.addField(ids[i], regexs[i], substs[i]); + } + return builder.build(); + } + + @Override + public CharSequenceTransformation[] newArray(int size) { + return new CharSequenceTransformation[size]; + } + }; +} diff --git a/core/java/android/service/autofill/CustomDescription.java b/core/java/android/service/autofill/CustomDescription.java new file mode 100644 index 000000000000..51530d61c9f5 --- /dev/null +++ b/core/java/android/service/autofill/CustomDescription.java @@ -0,0 +1,218 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.autofill; + +import static android.view.autofill.Helper.sDebug; + +import android.annotation.NonNull; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Log; +import android.util.SparseArray; +import android.widget.RemoteViews; + +import com.android.internal.util.Preconditions; + +/** + * Defines a custom description for the Save UI affordance. + * + * <p>This is useful when the autofill service needs to show a detailed view of what would be saved; + * for example, when the screen contains a credit card, it could display a logo of the credit card + * bank, the last for digits of the credit card number, and its expiration number. + * + * <p>A custom description is made of 2 parts: + * <ul> + * <li>A {@link RemoteViews presentation template} containing children views. + * <li>{@link Transformation Transformations} to populate the children views. + * </ul> + * + * <p>For the credit card example mentioned above, the (simplified) template would be: + * + * <pre class="prettyprint"> + * <LinearLayout> + * <ImageView android:id="@+id/templateccLogo"/> + * <TextView android:id="@+id/templateCcNumber"/> + * <TextView android:id="@+id/templateExpDate"/> + * </LinearLayout> + * </pre> + * + * <p>Which in code translates to: + * + * <pre class="prettyprint"> + * CustomDescription.Builder buider = new Builder(new RemoteViews(pgkName, R.layout.cc_template); + * </pre> + * + * <p>Then the value of each of the 3 children would be changed at runtime based on the the value of + * the screen fields and the {@link Transformation Transformations}: + * + * <pre class="prettyprint"> + * // Image child - different logo for each bank, based on credit card prefix + * builder.addChild(R.id.templateccLogo, + * new ImageTransformation.Builder(ccNumberId) + * .addOption("^4815.*$", R.drawable.ic_credit_card_logo1) + * .addOption("^1623.*$", R.drawable.ic_credit_card_logo2) + * .addOption("^42.*$", R.drawable.ic_credit_card_logo3); + * // Masked credit card number (as .....LAST_4_DIGITS) + * builder.addChild(R.id.templateCcNumber, new CharSequenceTransformation.Builder() + * .addField(ccNumberId, "^.*(\\d\\d\\d\\d)$", "...$1") + * // Expiration date as MM / YYYY: + * builder.addChild(R.id.templateExpDate, new CharSequenceTransformation.Builder() + * .addField(ccExpMonthId, "^(\\d\\d)$", "Exp: $1") + * .addField(ccExpYearId, "^(\\d\\d)$", "/$1"); + * </pre> + * + * <p>See {@link ImageTransformation}, {@link CharSequenceTransformation} for more info about these + * transformations. + */ +// TODO(b/62534917): add integration tests +public final class CustomDescription implements Parcelable { + + private static final String TAG = "CustomDescription"; + + private final RemoteViews mPresentation; + private final SparseArray<InternalTransformation> mTransformations; + + private CustomDescription(Builder builder) { + mPresentation = builder.mPresentation; + mTransformations = builder.mTransformations; + } + + /** @hide */ + public RemoteViews getPresentation(ValueFinder finder) { + // TODO(b/62534917): need to handler errors, like not finding the ID + if (mTransformations != null) { + final int size = mTransformations.size(); + if (sDebug) Log.d(TAG, "getPresentation(): applying " + size + " transformations"); + for (int i = 0; i < size; i++) { + final int id = mTransformations.keyAt(i); + final InternalTransformation transformation = mTransformations.valueAt(i); + if (sDebug) Log.d(TAG, "#" + i + ": " + transformation); + transformation.apply(finder, mPresentation, id); + } + } + return mPresentation; + } + + /** + * Builder for {@link CustomDescription} objects. + */ + public static class Builder { + private final RemoteViews mPresentation; + + private SparseArray<InternalTransformation> mTransformations; + + /** + * Default constructor. + * + * @param parentPresentation template presentation with (optional) children views. + */ + public Builder(RemoteViews parentPresentation) { + mPresentation = parentPresentation; + } + + /** + * Adds a transformation to replace the value of a child view with the fields in the + * screen. + * + * @param id view id of the children view. + * @param transformation an implementation provided by the Android System. + * @return this builder. + * @throws IllegalArgumentException if {@code transformation} is not a class provided + * by the Android System. + */ + public Builder addChild(int id, @NonNull Transformation transformation) { + Preconditions.checkArgument((transformation instanceof InternalTransformation), + "not provided by Android System: " + transformation); + if (mTransformations == null) { + mTransformations = new SparseArray<>(); + } + mTransformations.put(id, (InternalTransformation) transformation); + return this; + } + + /** + * Creates a new {@link CustomDescription} instance. + */ + public CustomDescription build() { + return new CustomDescription(this); + } + } + + ///////////////////////////////////// + // Object "contract" methods. // + ///////////////////////////////////// + @Override + public String toString() { + if (!sDebug) return super.toString(); + + return new StringBuilder("CustomDescription: [presentation=") + .append(mPresentation) + .append(", transformations=").append(mTransformations) + .append("]").toString(); + } + + ///////////////////////////////////// + // Parcelable "contract" methods. // + ///////////////////////////////////// + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelable(mPresentation, flags); + if (mTransformations == null) { + dest.writeIntArray(null); + } else { + final int size = mTransformations.size(); + final int[] ids = new int[size]; + final InternalTransformation[] values = new InternalTransformation[size]; + for (int i = 0; i < size; i++) { + ids[i] = mTransformations.keyAt(i); + values[i] = mTransformations.valueAt(i); + } + dest.writeIntArray(ids); + dest.writeParcelableArray(values, flags); + } + } + public static final Parcelable.Creator<CustomDescription> CREATOR = + new Parcelable.Creator<CustomDescription>() { + @Override + public CustomDescription createFromParcel(Parcel parcel) { + // Always go through the builder to ensure the data ingested by + // the system obeys the contract of the builder to avoid attacks + // using specially crafted parcels. + final Builder builder = new Builder(parcel.readParcelable(null)); + final int[] ids = parcel.createIntArray(); + if (ids != null) { + final InternalTransformation[] values = + parcel.readParcelableArray(null, InternalTransformation.class); + final int size = ids.length; + for (int i = 0; i < size; i++) { + builder.addChild(ids[i], values[i]); + } + } + return builder.build(); + } + + @Override + public CustomDescription[] newArray(int size) { + return new CustomDescription[size]; + } + }; +} diff --git a/core/java/android/service/autofill/ImageTransformation.java b/core/java/android/service/autofill/ImageTransformation.java new file mode 100644 index 000000000000..9f6eedc8b3a2 --- /dev/null +++ b/core/java/android/service/autofill/ImageTransformation.java @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.autofill; + +import static android.view.autofill.Helper.sDebug; + +import android.annotation.NonNull; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.ArrayMap; +import android.util.Log; +import android.view.autofill.AutofillId; +import android.widget.ImageView; +import android.widget.RemoteViews; + +import com.android.internal.util.Preconditions; + +/** + * Replaces the content of a child {@link ImageView} of a + * {@link RemoteViews presentation template} with the first image that matches a regular expression + * (regex). + * + * <p>Typically used to display credit card logos. Example: + * + * <pre class="prettyprint"> + * new ImageTransformation.Builder(ccNumberId) + * .addOption("^4815.*$", R.drawable.ic_credit_card_logo1) + * .addOption("^1623.*$", R.drawable.ic_credit_card_logo2) + * .addOption("^42.*$", R.drawable.ic_credit_card_logo3) + * .build(); + * </pre> + * + * <p>There is no imposed limit in the number of options, but keep in mind that regexs are + * expensive to evaluate, so try to: + * <ul> + * <li>Use the minimum number of regex per image. + * <li>Add the most common images first. + * </ul> + */ +//TODO(b/62534917): add unit tests +public final class ImageTransformation extends InternalTransformation implements Parcelable { + private static final String TAG = "ImageTransformation"; + + private final AutofillId mId; + private final ArrayMap<String, Integer> mOptions; + + private ImageTransformation(Builder builder) { + mId = builder.mId; + mOptions = builder.mOptions; + } + + /** @hide */ + @Override + public void apply(@NonNull ValueFinder finder, @NonNull RemoteViews parentTemplate, + int childViewId) { + final String value = finder.findByAutofillId(mId); + if (value == null) { + Log.w(TAG, "No view for id " + mId); + return; + } + final int size = mOptions.size(); + if (sDebug) { + Log.d(TAG, size + " multiple options on id " + childViewId + " to compare against " + + value); + } + + for (int i = 0; i < size; i++) { + final String regex = mOptions.keyAt(i); + if (value.matches(regex)) { + Log.d(TAG, "Found match at " + i + ": " + regex); + parentTemplate.setImageViewResource(childViewId, mOptions.valueAt(i)); + return; + } + } + Log.w(TAG, "No match for " + value); + } + + /** + * Builder for {@link ImageTransformation} objects. + */ + public static class Builder { + private final AutofillId mId; + private ArrayMap<String, Integer> mOptions; + private boolean mDestroyed; + + /** + * Default constructor. + * + * @param id id of the screen field that will be used to evaluate whether the image should + * be used. + */ + //TODO(b/62534917): add a regex/resid so we force it to have at least one + // (and then remove the check for empty from build()) + public Builder(@NonNull AutofillId id) { + mId = Preconditions.checkNotNull(id); + } + + /** + * Adds an option to replace the child view with a different image when the regex matches. + * + * @param regex regular expression defining what should be matched to use this image. + * @param resId resource id of the image (in the autofill service's package). The + * {@link RemoteViews presentation} must contain a {@link ImageView} child with that id. + * + * @return this build + */ + public Builder addOption(String regex, int resId) { + //TODO(b/62534917): throw exception if regex / resId are invalid + throwIfDestroyed(); + if (mOptions == null) { + mOptions = new ArrayMap<>(); + } + mOptions.put(regex, resId); + return this; + } + + /** + * Creates a new {@link ImageTransformation} instance. + * + * @throws IllegalStateException if no call to {@link #addOption(String, int)} was made. + */ + public ImageTransformation build() { + throwIfDestroyed(); + Preconditions.checkState(mOptions != null && !mOptions.isEmpty(), + "Must add at least one option"); + mDestroyed = true; + return new ImageTransformation(this); + } + + private void throwIfDestroyed() { + Preconditions.checkState(!mDestroyed, "Already called build()"); + } + } + + ///////////////////////////////////// + // Object "contract" methods. // + ///////////////////////////////////// + @Override + public String toString() { + if (!sDebug) return super.toString(); + + return "ImageTransformation: [id=" + mId + ", options=" + mOptions + "]"; + } + + ///////////////////////////////////// + // Parcelable "contract" methods. // + ///////////////////////////////////// + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeParcelable(mId, flags); + if (mOptions == null) { + parcel.writeStringArray(null); + return; + } + final int size = mOptions.size(); + final String[] regexs = new String[size]; + final int[] resIds = new int[size]; + for (int i = 0; i < size; i++) { + regexs[i] = mOptions.keyAt(i); + resIds[i] = mOptions.valueAt(i); + } + parcel.writeStringArray(regexs); + parcel.writeIntArray(resIds); + } + + public static final Parcelable.Creator<ImageTransformation> CREATOR = + new Parcelable.Creator<ImageTransformation>() { + @Override + public ImageTransformation createFromParcel(Parcel parcel) { + // Always go through the builder to ensure the data ingested by + // the system obeys the contract of the builder to avoid attacks + // using specially crafted parcels. + final ImageTransformation.Builder builder = + new ImageTransformation.Builder(parcel.readParcelable(null)); + final String[] regexs = parcel.createStringArray(); + if (regexs != null) { + final int[] resIds = parcel.createIntArray(); + final int size = regexs.length; + for (int i = 0; i < size; i++) { + builder.addOption(regexs[i], resIds[i]); + } + } + return builder.build(); + } + + @Override + public ImageTransformation[] newArray(int size) { + return new ImageTransformation[size]; + } + }; +} diff --git a/core/java/android/service/autofill/InternalTransformation.java b/core/java/android/service/autofill/InternalTransformation.java new file mode 100644 index 000000000000..3e51f87c7280 --- /dev/null +++ b/core/java/android/service/autofill/InternalTransformation.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.service.autofill; + +import android.annotation.NonNull; +import android.os.Parcelable; +import android.widget.RemoteViews; + +/** @hide */ +abstract class InternalTransformation implements Transformation, Parcelable { + + /** + * Applies this transformation to a child view of a {@link RemoteViews presentation template}. + * + * @param finder object used to find the value of a field in the screen. + * @param template the {@link RemoteViews presentation template}. + * @param childViewId resource id of the child view inside the template. + * + * @hide + */ + abstract void apply(@NonNull ValueFinder finder, @NonNull RemoteViews template, + int childViewId); +} diff --git a/core/java/android/service/autofill/InternalValidator.java b/core/java/android/service/autofill/InternalValidator.java new file mode 100644 index 000000000000..37ef96fd82ea --- /dev/null +++ b/core/java/android/service/autofill/InternalValidator.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.service.autofill; + +import android.annotation.NonNull; +import android.os.Parcelable; + +/** @hide */ +public abstract class InternalValidator implements Validator, Parcelable { + + /** + * Decides whether the contents of the screen are valid. + * + * @param finder object used to find the value of a field in the screen. + * @return {@code true} if the contents are valid, {@code false} otherwise. + * + * @hide + */ + public abstract boolean isValid(@NonNull ValueFinder finder); +} diff --git a/core/java/android/service/autofill/LuhnChecksumValidator.java b/core/java/android/service/autofill/LuhnChecksumValidator.java new file mode 100644 index 000000000000..713f0f9a963e --- /dev/null +++ b/core/java/android/service/autofill/LuhnChecksumValidator.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.autofill; + +import static android.view.autofill.Helper.sDebug; + +import android.annotation.NonNull; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; +import android.util.Log; +import android.view.autofill.AutofillId; + +import com.android.internal.util.Preconditions; + +/** + * Validator that returns {@code true} if the number created by concatenating all given fields + * pass a Luhn algorithm checksum. + * + * <p>See {@link SaveInfo.Builder#setValidator(Validator)} for examples. + */ +public final class LuhnChecksumValidator extends InternalValidator implements Parcelable { + private static final String TAG = "LuhnChecksumValidator"; + + private final AutofillId[] mIds; + + /** + * Default constructor. + * + * @param ids id of fields that comprises the number to be checked. + */ + public LuhnChecksumValidator(@NonNull AutofillId... ids) { + mIds = Preconditions.checkArrayElementsNotNull(ids, "ids"); + } + + /** @hide */ + @Override + public boolean isValid(@NonNull ValueFinder finder) { + if (mIds == null || mIds.length == 0) return false; + + final StringBuilder number = new StringBuilder(); + for (AutofillId id : mIds) { + final String partialNumber = finder.findByAutofillId(id); + if (partialNumber == null) { + if (sDebug) Log.d(TAG, "No partial number for id " + id); + return false; + } + number.append(partialNumber); + } + final boolean isValid = TextUtils.isDigitsOnly(number.toString()); + if (sDebug) Log.d(TAG, "Is valid: " + isValid); + // TODO(b/62534917): proper implementation - copy & paste code from: + // PaymentUtils.java + // PaymentUtilsTest.java + return isValid; + } + + ///////////////////////////////////// + // Parcelable "contract" methods. // + ///////////////////////////////////// + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeParcelableArray(mIds, flags); + } + + public static final Parcelable.Creator<LuhnChecksumValidator> CREATOR = + new Parcelable.Creator<LuhnChecksumValidator>() { + @Override + public LuhnChecksumValidator createFromParcel(Parcel parcel) { + return new LuhnChecksumValidator(parcel.readParcelableArray(null, AutofillId.class)); + } + + @Override + public LuhnChecksumValidator[] newArray(int size) { + return new LuhnChecksumValidator[size]; + } + }; +} diff --git a/core/java/android/service/autofill/OptionalValidators.java b/core/java/android/service/autofill/OptionalValidators.java new file mode 100644 index 000000000000..c9dd1d40e0aa --- /dev/null +++ b/core/java/android/service/autofill/OptionalValidators.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.autofill; + +import static android.view.autofill.Helper.sDebug; + +import android.annotation.NonNull; +import android.os.Parcel; +import android.os.Parcelable; + +import com.android.internal.util.Preconditions; + +/** + * Compound validator that returns {@code true} on {@link #isValid(ValueFinder)} if any + * of its subvalidators returns {@code true} as well. + * + * <p>Used to implement an {@code OR} logical operation. + * + * @hide + */ +final class OptionalValidators extends InternalValidator { + + private final InternalValidator[] mValidators; + + OptionalValidators(@NonNull InternalValidator[] validators) { + mValidators = Preconditions.checkArrayElementsNotNull(validators, "validators"); + } + + @Override + public boolean isValid(@NonNull ValueFinder finder) { + if (mValidators == null) { + return true; + } + // TODO(b/62534917): handle errors, like not finding the ID + + for (InternalValidator validator : mValidators) { + final boolean valid = validator.isValid(finder); + if (valid) return true; + } + + return false; + } + + ///////////////////////////////////// + // Object "contract" methods. // + ///////////////////////////////////// + @Override + public String toString() { + if (!sDebug) return super.toString(); + + return new StringBuilder("OptionalValidators: [validators=").append(mValidators) + .append("]") + .toString(); + } + + ///////////////////////////////////// + // Parcelable "contract" methods. // + ///////////////////////////////////// + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelableArray(mValidators, flags); + } + + public static final Parcelable.Creator<OptionalValidators> CREATOR = + new Parcelable.Creator<OptionalValidators>() { + @Override + public OptionalValidators createFromParcel(Parcel parcel) { + return new OptionalValidators(parcel + .readParcelableArray(null, InternalValidator.class)); + } + + @Override + public OptionalValidators[] newArray(int size) { + return new OptionalValidators[size]; + } + }; +} diff --git a/core/java/android/service/autofill/RequiredValidators.java b/core/java/android/service/autofill/RequiredValidators.java new file mode 100644 index 000000000000..f2b7db8af7a8 --- /dev/null +++ b/core/java/android/service/autofill/RequiredValidators.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.autofill; + +import static android.view.autofill.Helper.sDebug; + +import android.annotation.NonNull; +import android.os.Parcel; +import android.os.Parcelable; + +import com.android.internal.util.Preconditions; + +/** + * Compound validator that only returns {@code true} on {@link #isValid(ValueFinder)} if all + * of its subvalidators return {@code true} as well. + * + * <p>Used to implement an {@code AND} logical operation. + * + * @hide + */ +final class RequiredValidators extends InternalValidator { + + private final InternalValidator[] mValidators; + + RequiredValidators(@NonNull InternalValidator[] validators) { + mValidators = Preconditions.checkArrayElementsNotNull(validators, "validators"); + } + + @Override + public boolean isValid(@NonNull ValueFinder finder) { + if (mValidators == null) { + return true; + } + // TODO(b/62534917): handle errors, like not finding the ID + for (InternalValidator validator : mValidators) { + final boolean valid = validator.isValid(finder); + if (!valid) return false; + } + return true; + } + + ///////////////////////////////////// + // Object "contract" methods. // + ///////////////////////////////////// + @Override + public String toString() { + if (!sDebug) return super.toString(); + + return new StringBuilder("RequiredValidators: [validators=").append(mValidators) + .append("]") + .toString(); + } + + ///////////////////////////////////// + // Parcelable "contract" methods. // + ///////////////////////////////////// + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelableArray(mValidators, flags); + } + + public static final Parcelable.Creator<RequiredValidators> CREATOR = + new Parcelable.Creator<RequiredValidators>() { + @Override + public RequiredValidators createFromParcel(Parcel parcel) { + return new RequiredValidators(parcel + .readParcelableArray(null, InternalValidator.class)); + } + + @Override + public RequiredValidators[] newArray(int size) { + return new RequiredValidators[size]; + } + }; +} diff --git a/core/java/android/service/autofill/SaveInfo.java b/core/java/android/service/autofill/SaveInfo.java index 95d393b0234c..41491735df66 100644 --- a/core/java/android/service/autofill/SaveInfo.java +++ b/core/java/android/service/autofill/SaveInfo.java @@ -122,9 +122,13 @@ import java.util.Arrays; * * <p>The service can also customize some aspects of the save UI affordance: * <ul> - * <li>Add a subtitle by calling {@link Builder#setDescription(CharSequence)}. + * <li>Add a simple subtitle by calling {@link Builder#setDescription(CharSequence)}. + * <li>Add a customized subtitle by calling + * {@link Builder#setCustomDescription(CustomDescription)}. * <li>Customize the button used to reject the save request by calling * {@link Builder#setNegativeAction(int, IntentSender)}. + * <li>Decide whether the UI should be shown based on the user input validation by calling + * {@link Builder#setValidator(Validator)}. * </ul> */ public final class SaveInfo implements Parcelable { @@ -222,6 +226,8 @@ public final class SaveInfo implements Parcelable { private final AutofillId[] mOptionalIds; private final CharSequence mDescription; private final int mFlags; + private final CustomDescription mCustomDescription; + private final InternalValidator mValidator; private SaveInfo(Builder builder) { mType = builder.mType; @@ -231,6 +237,8 @@ public final class SaveInfo implements Parcelable { mOptionalIds = builder.mOptionalIds; mDescription = builder.mDescription; mFlags = builder.mFlags; + mCustomDescription = builder.mCustomDescription; + mValidator = builder.mValidator; } /** @hide */ @@ -268,6 +276,18 @@ public final class SaveInfo implements Parcelable { return mDescription; } + /** @hide */ + @Nullable + public CustomDescription getCustomDescription() { + return mCustomDescription; + } + + /** @hide */ + @Nullable + public InternalValidator getValidator() { + return mValidator; + } + /** * A builder for {@link SaveInfo} objects. */ @@ -281,12 +301,14 @@ public final class SaveInfo implements Parcelable { private CharSequence mDescription; private boolean mDestroyed; private int mFlags; + private CustomDescription mCustomDescription; + private InternalValidator mValidator; /** * Creates a new builder. * - * @param type the type of information the associated {@link FillResponse} represents, can - * be any combination of {@link SaveInfo#SAVE_DATA_TYPE_GENERIC}, + * @param type the type of information the associated {@link FillResponse} represents. It + * can be any combination of {@link SaveInfo#SAVE_DATA_TYPE_GENERIC}, * {@link SaveInfo#SAVE_DATA_TYPE_PASSWORD}, * {@link SaveInfo#SAVE_DATA_TYPE_ADDRESS}, {@link SaveInfo#SAVE_DATA_TYPE_CREDIT_CARD}, * {@link SaveInfo#SAVE_DATA_TYPE_USERNAME}, or @@ -354,21 +376,46 @@ public final class SaveInfo implements Parcelable { * * @param description a succint description. * @return This Builder. + * + * @throws IllegalStateException if this call was made after calling + * {@link #setCustomDescription(CustomDescription)}. */ public @NonNull Builder setDescription(@Nullable CharSequence description) { throwIfDestroyed(); + Preconditions.checkState(mCustomDescription == null, + "Can call setDescription() or setCustomDescription(), but not both"); mDescription = description; return this; } /** + * Sets a custom description to be shown in the UI when the user is asked to save. + * + * <p>Typically used when the service must show more info about the object being saved, + * like a credit card logo, masked number, and expiration date. + * + * @param customDescription the custom description. + * @return This Builder. + * + * @throws IllegalStateException if this call was made after calling + * {@link #setDescription(CharSequence)}. + */ + public @NonNull Builder setCustomDescription(@NonNull CustomDescription customDescription) { + throwIfDestroyed(); + Preconditions.checkState(mDescription == null, + "Can call setDescription() or setCustomDescription(), but not both"); + mCustomDescription = customDescription; + return this; + } + + /** * Sets the style and listener for the negative save action. * - * <p>This allows a fill-provider to customize the style and be + * <p>This allows an autofill service to customize the style and be * notified when the user selects the negative action in the save * UI. Note that selecting the negative action regardless of its style * and listener being customized would dismiss the save UI and if a - * custom listener intent is provided then this intent will be + * custom listener intent is provided then this intent is * started. The default style is {@link #NEGATIVE_BUTTON_STYLE_CANCEL}</p> * * @param style The action style. @@ -393,6 +440,74 @@ public final class SaveInfo implements Parcelable { } /** + * Sets an object used to validate the user input - if the input is not valid, the Save UI + * affordance is not shown. + * + * <p>Typically used to validate credit card numbers. Examples: + * + * <p>Validator for a credit number that must have exactly 16 digits: + * + * <pre class="prettyprint"> + * Validator validator = new SimpleRegexValidator(ccNumberId, "^\\d{16}$") + * </pre> + * + * <p>Validator for a credit number that must pass a Luhn checksum and either have + * 16 digits, or 15 digits starting with 108: + * + * <pre class="prettyprint"> + * import android.service.autofill.Validators; + * + * Validator validator = + * and( + * new LuhnChecksumValidator(ccNumberId), + * or( + * new SimpleRegexValidator(ccNumberId, "^\\d{16}$"), + * new SimpleRegexValidator(ccNumberId, "^108\\d{12}$") + * ) + * ); + * </pre> + * + * <p><b>NOTE: </b>the example above is just for illustrative purposes; the same validator + * could be created using a single regex for the {@code OR} part: + * + * <pre class="prettyprint"> + * Validator validator = + * and( + * new LuhnChecksumValidator(ccNumberId), + * new SimpleRegexValidator(ccNumberId, "^(\\d{16}|108\\d{12})$") + * ); + * </pre> + * + * <p>Validator for a credit number contained in just 4 fields and that must have exactly + * 4 digits on each field: + * + * <pre class="prettyprint"> + * import android.service.autofill.Validators; + * + * Validator validator = + * and( + * new SimpleRegexValidator.(ccNumberId1, "^\\d{4}$"), + * new SimpleRegexValidator.(ccNumberId2, "^\\d{4}$"), + * new SimpleRegexValidator.(ccNumberId3, "^\\d{4}$"), + * new SimpleRegexValidator.(ccNumberId4, "^\\d{4}$") + * ); + * </pre> + * + * @param validator an implementation provided by the Android System. + * @return this builder. + * + * @throws IllegalArgumentException if {@code validator} is not a class provided + * by the Android System. + */ + public @NonNull Builder setValidator(@NonNull Validator validator) { + throwIfDestroyed(); + Preconditions.checkArgument((validator instanceof InternalValidator), + "not provided by Android System: " + validator); + mValidator = (InternalValidator) validator; + return this; + } + + /** * Builds a new {@link SaveInfo} instance. */ public SaveInfo build() { @@ -406,7 +521,6 @@ public final class SaveInfo implements Parcelable { throw new IllegalStateException("Already called #build()"); } } - } ///////////////////////////////////// @@ -424,6 +538,8 @@ public final class SaveInfo implements Parcelable { .append(DebugUtils.flagsToString(SaveInfo.class, "NEGATIVE_BUTTON_STYLE_", mNegativeButtonStyle)) .append(", mFlags=").append(mFlags) + .append(", mCustomDescription=").append(mCustomDescription) + .append(", validation=").append(mValidator) .append("]").toString(); } @@ -444,6 +560,8 @@ public final class SaveInfo implements Parcelable { parcel.writeParcelable(mNegativeActionListener, flags); parcel.writeParcelableArray(mOptionalIds, flags); parcel.writeCharSequence(mDescription); + parcel.writeParcelable(mCustomDescription, flags); + parcel.writeParcelable(mValidator, flags); parcel.writeInt(mFlags); } @@ -461,6 +579,14 @@ public final class SaveInfo implements Parcelable { builder.setOptionalIds(optionalIds); } builder.setDescription(parcel.readCharSequence()); + final CustomDescription customDescripton = parcel.readParcelable(null); + if (customDescripton != null) { + builder.setCustomDescription(customDescripton); + } + final InternalValidator validator = parcel.readParcelable(null); + if (validator != null) { + builder.setValidator(validator); + } builder.setFlags(parcel.readInt()); return builder.build(); } diff --git a/core/java/android/service/autofill/SimpleRegexValidator.java b/core/java/android/service/autofill/SimpleRegexValidator.java new file mode 100644 index 000000000000..ffe0076dfb34 --- /dev/null +++ b/core/java/android/service/autofill/SimpleRegexValidator.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.autofill; + +import static android.view.autofill.Helper.sDebug; + +import android.annotation.NonNull; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Log; +import android.view.autofill.AutofillId; + +import com.android.internal.util.Preconditions; + +/** + * Defines if a field is valid based on a regular expression (regex). + * + * <p>See {@link SaveInfo.Builder#setValidator(Validator)} for examples. + */ +public final class SimpleRegexValidator extends InternalValidator implements Parcelable { + + private static final String TAG = "SimpleRegexValidator"; + + private final AutofillId mId; + private final String mRegex; + + /** + * Default constructor. + * + * @param id id of the field whose regex is applied to. + * @param regex regular expression that defines the result + * of the validator: if the regex matches the contents of + * the field identified by {@code id}, it returns {@code true}; otherwise, it + * returns {@code false}. + */ + public SimpleRegexValidator(@NonNull AutofillId id, @NonNull String regex) { + mId = Preconditions.checkNotNull(id); + //TODO(b/62534917): throw exception if regex is invalid + mRegex = Preconditions.checkNotNull(regex); + } + + /** @hide */ + @Override + public boolean isValid(@NonNull ValueFinder finder) { + final String value = finder.findByAutofillId(mId); + if (value == null) { + Log.w(TAG, "No view for id " + mId); + return false; + } + final boolean valid = value.matches(mRegex); + if (sDebug) Log.d(TAG, "isValid(): " + valid); + return valid; + } + + ///////////////////////////////////// + // Object "contract" methods. // + ///////////////////////////////////// + @Override + public String toString() { + if (!sDebug) return super.toString(); + + return "SimpleRegexValidator: [id=" + mId + ", regex=" + mRegex + "]"; + } + + ///////////////////////////////////// + // Parcelable "contract" methods. // + ///////////////////////////////////// + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeParcelable(mId, flags); + parcel.writeString(mRegex); + } + + public static final Parcelable.Creator<SimpleRegexValidator> CREATOR = + new Parcelable.Creator<SimpleRegexValidator>() { + @Override + public SimpleRegexValidator createFromParcel(Parcel parcel) { + return new SimpleRegexValidator(parcel.readParcelable(null), parcel.readString()); + } + + @Override + public SimpleRegexValidator[] newArray(int size) { + return new SimpleRegexValidator[size]; + } + }; +} diff --git a/core/java/android/service/autofill/Transformation.java b/core/java/android/service/autofill/Transformation.java new file mode 100644 index 000000000000..63b679d87259 --- /dev/null +++ b/core/java/android/service/autofill/Transformation.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.service.autofill; + +/** + * Helper class used to change a child view of a {@link RemoteViews presentation template} at + * runtime, using the values of fields contained in the screen. + * + * <p>Typically used by {@link CustomDescription} to provide a customized Save UI affordance. + */ +public interface Transformation { +} diff --git a/core/java/android/service/autofill/Validator.java b/core/java/android/service/autofill/Validator.java new file mode 100644 index 000000000000..854aa1e69db7 --- /dev/null +++ b/core/java/android/service/autofill/Validator.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.service.autofill; + +/** + * Helper class used to define whether the contents of a screen are valid. + * + * <p>Typically used to avoid displaying the Save UI affordance when the user input is invalid. + */ +public interface Validator { +} diff --git a/core/java/android/service/autofill/Validators.java b/core/java/android/service/autofill/Validators.java new file mode 100644 index 000000000000..51b503c21690 --- /dev/null +++ b/core/java/android/service/autofill/Validators.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.service.autofill; + +import android.annotation.NonNull; + +import com.android.internal.util.Preconditions; + +/** + * Factory for {@link Validator} operations. + * + * <p>See {@link SaveInfo.Builder#setValidator(Validator)} for examples. + */ +public final class Validators { + + private Validators() { + throw new UnsupportedOperationException("contains static methods only"); + } + + /** + * Creates a validator that is only valid if all {@code validators} are valid. + * + * @throws IllegalArgumentException if any element of {@code validators} is an instance of a + * class that is not provided by the Android System. + */ + @NonNull + public static Validator and(@NonNull Validator...validators) { + return new RequiredValidators(getInternalValidators(validators)); + } + + /** + * Creates a validator that is valid if any of the {@code validators} is valid. + * + * @throws IllegalArgumentException if any element of {@code validators} is an instance of a + * class that is not provided by the Android System. + */ + @NonNull + public static Validator or(@NonNull Validator...validators) { + return new OptionalValidators(getInternalValidators(validators)); + } + + private static InternalValidator[] getInternalValidators(Validator[] validators) { + Preconditions.checkArrayElementsNotNull(validators, "validators"); + + final InternalValidator[] internals = new InternalValidator[validators.length]; + + for (int i = 0; i < validators.length; i++) { + Preconditions.checkArgument((validators[i] instanceof InternalValidator), + "element " + i + " not provided by Android System: " + validators[i]); + internals[i] = (InternalValidator) validators[i]; + } + return internals; + } +} diff --git a/core/java/android/service/autofill/ValueFinder.java b/core/java/android/service/autofill/ValueFinder.java new file mode 100644 index 000000000000..d02a35890545 --- /dev/null +++ b/core/java/android/service/autofill/ValueFinder.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.service.autofill; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.view.autofill.AutofillId; + +/** + * Helper object used to obtain the value of a field in the screen being autofilled. + * + * @hide + */ +public interface ValueFinder { + + /** + * Gets the value of a field, or {@code null} when not found. + */ + @Nullable String findByAutofillId(@NonNull AutofillId id); +} diff --git a/core/java/android/service/euicc/EuiccService.java b/core/java/android/service/euicc/EuiccService.java index 26f85288699a..0c2e4b7ada26 100644 --- a/core/java/android/service/euicc/EuiccService.java +++ b/core/java/android/service/euicc/EuiccService.java @@ -317,6 +317,21 @@ public abstract class EuiccService extends Service { public abstract int onEraseSubscriptions(int slotId); /** + * Ensure that subscriptions will be retained on the next factory reset. + * + * <p>Called directly before a factory reset. Assumes that a normal factory reset will lead to + * profiles being erased on first boot (to cover fastboot/recovery wipes), so the implementation + * should persist some bit that will remain accessible after the factory reset to bypass this + * flow when this method is called. + * + * @param slotId ID of the SIM slot to use for the operation. This is currently not populated + * but is here to future-proof the APIs. + * @return the result of the operation. May be one of the predefined {@code RESULT_} constants + * or any implementation-specific code starting with {@link #RESULT_FIRST_USER}. + */ + public abstract int onRetainSubscriptionsForFactoryReset(int slotId); + + /** * Wrapper around IEuiccService that forwards calls to implementations of {@link EuiccService}. */ private class IEuiccServiceWrapper extends IEuiccService.Stub { @@ -488,5 +503,21 @@ public abstract class EuiccService extends Service { } }); } + + @Override + public void retainSubscriptionsForFactoryReset(int slotId, + IRetainSubscriptionsForFactoryResetCallback callback) { + mExecutor.execute(new Runnable() { + @Override + public void run() { + int result = EuiccService.this.onRetainSubscriptionsForFactoryReset(slotId); + try { + callback.onComplete(result); + } catch (RemoteException e) { + // Can't communicate with the phone process; ignore. + } + } + }); + } } } diff --git a/core/java/android/service/euicc/IEuiccService.aidl b/core/java/android/service/euicc/IEuiccService.aidl index 18ea915d5dc7..e10dd8cdf616 100644 --- a/core/java/android/service/euicc/IEuiccService.aidl +++ b/core/java/android/service/euicc/IEuiccService.aidl @@ -24,6 +24,7 @@ import android.service.euicc.IGetDownloadableSubscriptionMetadataCallback; import android.service.euicc.IGetEidCallback; import android.service.euicc.IGetEuiccInfoCallback; import android.service.euicc.IGetEuiccProfileInfoListCallback; +import android.service.euicc.IRetainSubscriptionsForFactoryResetCallback; import android.service.euicc.ISwitchToSubscriptionCallback; import android.service.euicc.IUpdateSubscriptionNicknameCallback; import android.telephony.euicc.DownloadableSubscription; @@ -46,4 +47,6 @@ oneway interface IEuiccService { void updateSubscriptionNickname(int slotId, String iccid, String nickname, in IUpdateSubscriptionNicknameCallback callback); void eraseSubscriptions(int slotId, in IEraseSubscriptionsCallback callback); + void retainSubscriptionsForFactoryReset( + int slotId, in IRetainSubscriptionsForFactoryResetCallback callback); }
\ No newline at end of file diff --git a/core/java/android/service/euicc/IRetainSubscriptionsForFactoryResetCallback.aidl b/core/java/android/service/euicc/IRetainSubscriptionsForFactoryResetCallback.aidl new file mode 100644 index 000000000000..127683059c02 --- /dev/null +++ b/core/java/android/service/euicc/IRetainSubscriptionsForFactoryResetCallback.aidl @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.euicc; + +/** @hide */ +oneway interface IRetainSubscriptionsForFactoryResetCallback { + void onComplete(int result); +}
\ No newline at end of file diff --git a/core/java/com/android/internal/policy/PipSnapAlgorithm.java b/core/java/com/android/internal/policy/PipSnapAlgorithm.java index 95d714f1c3c7..749d00c136ec 100644 --- a/core/java/com/android/internal/policy/PipSnapAlgorithm.java +++ b/core/java/com/android/internal/policy/PipSnapAlgorithm.java @@ -49,9 +49,6 @@ public class PipSnapAlgorithm { // Allows snapping on the long edge in each orientation and magnets towards corners private static final int SNAP_MODE_LONG_EDGE_MAGNET_CORNERS = 4; - // The friction multiplier to control how slippery the PIP is when flung - private static final float SCROLL_FRICTION_MULTIPLIER = 8f; - // Threshold to magnet to a corner private static final float CORNER_MAGNET_THRESHOLD = 0.3f; @@ -64,8 +61,8 @@ public class PipSnapAlgorithm { private final float mDefaultSizePercent; private final float mMinAspectRatioForMinSize; private final float mMaxAspectRatioForMinSize; + private final int mFlingDeceleration; - private Scroller mScroller; private int mOrientation = Configuration.ORIENTATION_UNDEFINED; private final int mMinimizedVisibleSize; @@ -81,6 +78,8 @@ public class PipSnapAlgorithm { mMaxAspectRatioForMinSize = res.getFloat( com.android.internal.R.dimen.config_pictureInPictureAspectRatioLimitForMinSize); mMinAspectRatioForMinSize = 1f / mMaxAspectRatioForMinSize; + mFlingDeceleration = mContext.getResources().getDimensionPixelSize( + com.android.internal.R.dimen.pip_fling_deceleration); onConfigurationChanged(); } @@ -107,20 +106,97 @@ public class PipSnapAlgorithm { * those for the given {@param stackBounds}. */ public Rect findClosestSnapBounds(Rect movementBounds, Rect stackBounds, float velocityX, - float velocityY) { - final Rect finalStackBounds = new Rect(stackBounds); - if (mScroller == null) { - final ViewConfiguration viewConfig = ViewConfiguration.get(mContext); - mScroller = new Scroller(mContext); - mScroller.setFriction(viewConfig.getScrollFriction() * SCROLL_FRICTION_MULTIPLIER); + float velocityY, Point dragStartPosition) { + final Rect intersectStackBounds = new Rect(stackBounds); + final Point intersect = getEdgeIntersect(stackBounds, movementBounds, velocityX, velocityY, + dragStartPosition); + intersectStackBounds.offsetTo(intersect.x, intersect.y); + return findClosestSnapBounds(movementBounds, intersectStackBounds); + } + + /** + * @return The point along the {@param movementBounds} that the PIP would intersect with based + * on the provided {@param velX}, {@param velY} along with the position of the PIP when + * the gesture started, {@param dragStartPosition}. + */ + public Point getEdgeIntersect(Rect stackBounds, Rect movementBounds, float velX, float velY, + Point dragStartPosition) { + final boolean isLandscape = mOrientation == Configuration.ORIENTATION_LANDSCAPE; + final int x = stackBounds.left; + final int y = stackBounds.top; + + // Find the line of movement the PIP is on. Line defined by: y = slope * x + yIntercept + final float slope = velY / velX; // slope = rise / run + final float yIntercept = y - slope * x; // rearrange line equation for yIntercept + // The PIP can have two intercept points: + // 1) Where the line intersects with one of the edges of the screen (vertical line) + Point vertPoint = new Point(); + // 2) Where the line intersects with the top or bottom of the screen (horizontal line) + Point horizPoint = new Point(); + + // Find the vertical line intersection, x will be one of the edges + vertPoint.x = velX > 0 ? movementBounds.right : movementBounds.left; + // Sub in x in our line equation to determine y position + vertPoint.y = findY(slope, yIntercept, vertPoint.x); + + // Find the horizontal line intersection, y will be the top or bottom of the screen + horizPoint.y = velY > 0 ? movementBounds.bottom : movementBounds.top; + // Sub in y in our line equation to determine x position + horizPoint.x = findX(slope, yIntercept, horizPoint.y); + + // Now pick one of these points -- first determine if we're flinging along the current edge. + // Only fling along current edge if it's a direction with space for the PIP to move to + int maxDistance; + if (isLandscape) { + maxDistance = velX > 0 + ? movementBounds.right - stackBounds.left + : stackBounds.left - movementBounds.left; + } else { + maxDistance = velY > 0 + ? movementBounds.bottom - stackBounds.top + : stackBounds.top - movementBounds.top; } - mScroller.fling(stackBounds.left, stackBounds.top, - (int) velocityX, (int) velocityY, - movementBounds.left, movementBounds.right, - movementBounds.top, movementBounds.bottom); - finalStackBounds.offsetTo(mScroller.getFinalX(), mScroller.getFinalY()); - mScroller.abortAnimation(); - return findClosestSnapBounds(movementBounds, finalStackBounds); + if (maxDistance > 0) { + // Only fling along the current edge if the start and end point are on the same side + final int startPoint = isLandscape ? dragStartPosition.y : dragStartPosition.x; + final int endPoint = isLandscape ? horizPoint.y : horizPoint.x; + final int center = movementBounds.centerX(); + if ((startPoint < center && endPoint < center) + || (startPoint > center && endPoint > center)) { + // We are flinging along the current edge, figure out how far it should travel + // based on velocity and assumed deceleration. + int distance = (int) (0 - Math.pow(isLandscape ? velX : velY, 2)) + / (2 * mFlingDeceleration); + distance = Math.min(distance, maxDistance); + // Adjust the point for the distance + if (isLandscape) { + horizPoint.x = stackBounds.left + (velX > 0 ? distance : -distance); + } else { + horizPoint.y = stackBounds.top + (velY > 0 ? distance : -distance); + } + return horizPoint; + } + } + // If we're not flinging along the current edge, find the closest point instead. + final double distanceVert = Math.hypot(vertPoint.x - x, vertPoint.y - y); + final double distanceHoriz = Math.hypot(horizPoint.x - x, horizPoint.y - y); + // Ensure that we're actually going somewhere + if (distanceVert == 0) { + return horizPoint; + } + if (distanceHoriz == 0) { + return vertPoint; + } + // Otherwise use the closest point + return Math.abs(distanceVert) > Math.abs(distanceHoriz) ? horizPoint : vertPoint; + } + + private int findY(float slope, float yIntercept, float x) { + return (int) ((slope * x) + yIntercept); + } + + private int findX(float slope, float yIntercept, float y) { + return (int) ((y - yIntercept) / slope); } /** diff --git a/core/jni/android_os_HwBinder.cpp b/core/jni/android_os_HwBinder.cpp index cdd3c094e009..6c6fa66c422e 100644 --- a/core/jni/android_os_HwBinder.cpp +++ b/core/jni/android_os_HwBinder.cpp @@ -370,7 +370,7 @@ static jobject JHwBinder_native_getService( if (transport != IServiceManager::Transport::HWBINDER && !vintfLegacy) { LOG(ERROR) << "service " << ifaceName << " declares transport method " << toString(transport) << " but framework expects hwbinder."; - signalExceptionForError(env, UNKNOWN_ERROR, true /* canThrowRemoteException */); + signalExceptionForError(env, NAME_NOT_FOUND, true /* canThrowRemoteException */); return NULL; } diff --git a/core/jni/android_os_Parcel.cpp b/core/jni/android_os_Parcel.cpp index cd7aae7d29dd..d35ddd0a87a8 100644 --- a/core/jni/android_os_Parcel.cpp +++ b/core/jni/android_os_Parcel.cpp @@ -90,25 +90,25 @@ void recycleJavaParcelObject(JNIEnv* env, jobject parcelObj) env->CallVoidMethod(parcelObj, gParcelOffsets.recycle); } -static jint android_os_Parcel_dataSize(JNIEnv* env, jclass clazz, jlong nativePtr) +static jint android_os_Parcel_dataSize(jlong nativePtr) { Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr); return parcel ? parcel->dataSize() : 0; } -static jint android_os_Parcel_dataAvail(JNIEnv* env, jclass clazz, jlong nativePtr) +static jint android_os_Parcel_dataAvail(jlong nativePtr) { Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr); return parcel ? parcel->dataAvail() : 0; } -static jint android_os_Parcel_dataPosition(JNIEnv* env, jclass clazz, jlong nativePtr) +static jint android_os_Parcel_dataPosition(jlong nativePtr) { Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr); return parcel ? parcel->dataPosition() : 0; } -static jint android_os_Parcel_dataCapacity(JNIEnv* env, jclass clazz, jlong nativePtr) +static jint android_os_Parcel_dataCapacity(jlong nativePtr) { Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr); return parcel ? parcel->dataCapacity() : 0; @@ -127,7 +127,7 @@ static jlong android_os_Parcel_setDataSize(JNIEnv* env, jclass clazz, jlong nati return 0; } -static void android_os_Parcel_setDataPosition(JNIEnv* env, jclass clazz, jlong nativePtr, jint pos) +static void android_os_Parcel_setDataPosition(jlong nativePtr, jint pos) { Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr); if (parcel != NULL) { @@ -146,7 +146,7 @@ static void android_os_Parcel_setDataCapacity(JNIEnv* env, jclass clazz, jlong n } } -static jboolean android_os_Parcel_pushAllowFds(JNIEnv* env, jclass clazz, jlong nativePtr, jboolean allowFds) +static jboolean android_os_Parcel_pushAllowFds(jlong nativePtr, jboolean allowFds) { Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr); jboolean ret = JNI_TRUE; @@ -156,7 +156,7 @@ static jboolean android_os_Parcel_pushAllowFds(JNIEnv* env, jclass clazz, jlong return ret; } -static void android_os_Parcel_restoreAllowFds(JNIEnv* env, jclass clazz, jlong nativePtr, jboolean lastValue) +static void android_os_Parcel_restoreAllowFds(jlong nativePtr, jboolean lastValue) { Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr); if (parcel != NULL) { @@ -398,7 +398,7 @@ static jbyteArray android_os_Parcel_readBlob(JNIEnv* env, jclass clazz, jlong na return ret; } -static jint android_os_Parcel_readInt(JNIEnv* env, jclass clazz, jlong nativePtr) +static jint android_os_Parcel_readInt(jlong nativePtr) { Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr); if (parcel != NULL) { @@ -407,7 +407,7 @@ static jint android_os_Parcel_readInt(JNIEnv* env, jclass clazz, jlong nativePtr return 0; } -static jlong android_os_Parcel_readLong(JNIEnv* env, jclass clazz, jlong nativePtr) +static jlong android_os_Parcel_readLong(jlong nativePtr) { Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr); if (parcel != NULL) { @@ -416,7 +416,7 @@ static jlong android_os_Parcel_readLong(JNIEnv* env, jclass clazz, jlong nativeP return 0; } -static jfloat android_os_Parcel_readFloat(JNIEnv* env, jclass clazz, jlong nativePtr) +static jfloat android_os_Parcel_readFloat(jlong nativePtr) { Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr); if (parcel != NULL) { @@ -425,7 +425,7 @@ static jfloat android_os_Parcel_readFloat(JNIEnv* env, jclass clazz, jlong nativ return 0; } -static jdouble android_os_Parcel_readDouble(JNIEnv* env, jclass clazz, jlong nativePtr) +static jdouble android_os_Parcel_readDouble(jlong nativePtr) { Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr); if (parcel != NULL) { @@ -673,7 +673,7 @@ static jlong android_os_Parcel_appendFrom(JNIEnv* env, jclass clazz, jlong thisN return thisParcel->getOpenAshmemSize(); } -static jboolean android_os_Parcel_hasFileDescriptors(JNIEnv* env, jclass clazz, jlong nativePtr) +static jboolean android_os_Parcel_hasFileDescriptors(jlong nativePtr) { jboolean ret = JNI_FALSE; Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr); @@ -747,7 +747,7 @@ static jlong android_os_Parcel_getGlobalAllocCount(JNIEnv* env, jclass clazz) return Parcel::getGlobalAllocCount(); } -static jlong android_os_Parcel_getBlobAshmemSize(JNIEnv* env, jclass clazz, jlong nativePtr) +static jlong android_os_Parcel_getBlobAshmemSize(jlong nativePtr) { Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr); if (parcel != NULL) { @@ -759,24 +759,24 @@ static jlong android_os_Parcel_getBlobAshmemSize(JNIEnv* env, jclass clazz, jlon // ---------------------------------------------------------------------------- static const JNINativeMethod gParcelMethods[] = { - // @FastNative + // @CriticalNative {"nativeDataSize", "(J)I", (void*)android_os_Parcel_dataSize}, - // @FastNative + // @CriticalNative {"nativeDataAvail", "(J)I", (void*)android_os_Parcel_dataAvail}, - // @FastNative + // @CriticalNative {"nativeDataPosition", "(J)I", (void*)android_os_Parcel_dataPosition}, - // @FastNative + // @CriticalNative {"nativeDataCapacity", "(J)I", (void*)android_os_Parcel_dataCapacity}, // @FastNative {"nativeSetDataSize", "(JI)J", (void*)android_os_Parcel_setDataSize}, - // @FastNative + // @CriticalNative {"nativeSetDataPosition", "(JI)V", (void*)android_os_Parcel_setDataPosition}, // @FastNative {"nativeSetDataCapacity", "(JI)V", (void*)android_os_Parcel_setDataCapacity}, - // @FastNative + // @CriticalNative {"nativePushAllowFds", "(JZ)Z", (void*)android_os_Parcel_pushAllowFds}, - // @FastNative + // @CriticalNative {"nativeRestoreAllowFds", "(JZ)V", (void*)android_os_Parcel_restoreAllowFds}, {"nativeWriteByteArray", "(J[BII)V", (void*)android_os_Parcel_writeByteArray}, @@ -796,13 +796,13 @@ static const JNINativeMethod gParcelMethods[] = { {"nativeCreateByteArray", "(J)[B", (void*)android_os_Parcel_createByteArray}, {"nativeReadByteArray", "(J[BI)Z", (void*)android_os_Parcel_readByteArray}, {"nativeReadBlob", "(J)[B", (void*)android_os_Parcel_readBlob}, - // @FastNative + // @CriticalNative {"nativeReadInt", "(J)I", (void*)android_os_Parcel_readInt}, - // @FastNative + // @CriticalNative {"nativeReadLong", "(J)J", (void*)android_os_Parcel_readLong}, - // @FastNative + // @CriticalNative {"nativeReadFloat", "(J)F", (void*)android_os_Parcel_readFloat}, - // @FastNative + // @CriticalNative {"nativeReadDouble", "(J)D", (void*)android_os_Parcel_readDouble}, {"nativeReadString", "(J)Ljava/lang/String;", (void*)android_os_Parcel_readString}, {"nativeReadStrongBinder", "(J)Landroid/os/IBinder;", (void*)android_os_Parcel_readStrongBinder}, @@ -821,7 +821,7 @@ static const JNINativeMethod gParcelMethods[] = { {"nativeUnmarshall", "(J[BII)J", (void*)android_os_Parcel_unmarshall}, {"nativeCompareData", "(JJ)I", (void*)android_os_Parcel_compareData}, {"nativeAppendFrom", "(JJII)J", (void*)android_os_Parcel_appendFrom}, - // @FastNative + // @CriticalNative {"nativeHasFileDescriptors", "(J)Z", (void*)android_os_Parcel_hasFileDescriptors}, {"nativeWriteInterfaceToken", "(JLjava/lang/String;)V", (void*)android_os_Parcel_writeInterfaceToken}, {"nativeEnforceInterface", "(JLjava/lang/String;)V", (void*)android_os_Parcel_enforceInterface}, @@ -829,6 +829,7 @@ static const JNINativeMethod gParcelMethods[] = { {"getGlobalAllocSize", "()J", (void*)android_os_Parcel_getGlobalAllocSize}, {"getGlobalAllocCount", "()J", (void*)android_os_Parcel_getGlobalAllocCount}, + // @CriticalNative {"nativeGetBlobAshmemSize", "(J)J", (void*)android_os_Parcel_getBlobAshmemSize}, }; diff --git a/core/res/res/anim/task_close_enter.xml b/core/res/res/anim/task_close_enter.xml index b07f470dd5f5..bea0ee51d593 100644 --- a/core/res/res/anim/task_close_enter.xml +++ b/core/res/res/anim/task_close_enter.xml @@ -18,7 +18,7 @@ --> <set xmlns:android="http://schemas.android.com/apk/res/android" - android:background="#ff000000" android:shareInterpolator="false" android:zAdjustment="normal"> + android:shareInterpolator="false" android:zAdjustment="normal"> <alpha android:fromAlpha="0.6" android:toAlpha="1.0" android:fillEnabled="true" android:fillBefore="true" android:fillAfter="true" diff --git a/core/res/res/anim/task_close_exit.xml b/core/res/res/anim/task_close_exit.xml index d23c74ffc160..b6a08070c3ce 100644 --- a/core/res/res/anim/task_close_exit.xml +++ b/core/res/res/anim/task_close_exit.xml @@ -18,7 +18,7 @@ --> <set xmlns:android="http://schemas.android.com/apk/res/android" - android:background="#ff000000" android:shareInterpolator="false" android:zAdjustment="top"> + android:shareInterpolator="false" android:zAdjustment="top"> <alpha android:fromAlpha="1.0" android:toAlpha="0" android:fillEnabled="true" android:fillBefore="true" android:fillAfter="true" diff --git a/core/res/res/layout/autofill_save.xml b/core/res/res/layout/autofill_save.xml index 90b74acf9459..7b8d92285c5a 100644 --- a/core/res/res/layout/autofill_save.xml +++ b/core/res/res/layout/autofill_save.xml @@ -65,6 +65,15 @@ </LinearLayout> + <!-- TODO(b/62534917) wrap content to fit exactly what was provided in the remote views ?--> + <LinearLayout + android:id="@+id/autofill_save_custom_subtitle" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:layout_marginTop="4dp" + android:visibility="gone"/> + <TextView android:id="@+id/autofill_save_subtitle" android:layout_width="fill_parent" diff --git a/core/res/res/layout/notification_template_right_icon.xml b/core/res/res/layout/notification_template_right_icon.xml index aa4e05b15d19..fbf75387b786 100644 --- a/core/res/res/layout/notification_template_right_icon.xml +++ b/core/res/res/layout/notification_template_right_icon.xml @@ -26,8 +26,9 @@ android:layout_gravity="top|end" android:layout_marginTop="36dp" android:layout_marginEnd="@dimen/notification_content_margin_end" - android:scaleType="centerCrop"/> - <ImageView android:id="@+id/reply_icon_action" + android:scaleType="centerCrop" + android:importantForAccessibility="no" /> + <ImageButton android:id="@+id/reply_icon_action" android:layout_width="16dp" android:layout_height="16dp" android:layout_gravity="top|end" @@ -36,6 +37,7 @@ android:background="@drawable/notification_reply_background" android:src="@drawable/ic_reply_notification" android:scaleType="center" + android:contentDescription="@string/notification_reply_button_accessibility" visiblity="gone"/> </FrameLayout> diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml index 5e2334d20da1..fa33d567983e 100644 --- a/core/res/res/values/dimens.xml +++ b/core/res/res/values/dimens.xml @@ -67,6 +67,9 @@ <!-- The amount to leave on-screen when the PIP is minimized. --> <dimen name="pip_minimized_visible_size">48dp</dimen> + <!-- The the PIP decelerates at while moving from a fling. --> + <dimen name="pip_fling_deceleration">-3000dp</dimen> + <!-- Min width for a tablet device --> <dimen name="min_xlarge_screen_width">800dp</dimen> diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index 2e9bd0897b69..18e8af7836d2 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -4671,6 +4671,9 @@ <!-- Primary ETWS (Earthquake and Tsunami Warning System) default message for test --> <string name="etws_primary_default_message_test">Emergency messages test</string> + <!-- Content description for the reply button in the notification area [CHAR LIMIT=NONE]--> + <string name="notification_reply_button_accessibility">Reply</string> + <!-- Primary ETWS (Earthquake and Tsunami Warning System) default message for others --> <string name="etws_primary_default_message_others"></string> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index c4e43a35b113..2599b2a84d07 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -1570,6 +1570,7 @@ <java-symbol type="dimen" name="docked_stack_divider_insets" /> <java-symbol type="dimen" name="docked_stack_minimize_thickness" /> <java-symbol type="dimen" name="pip_minimized_visible_size" /> + <java-symbol type="dimen" name="pip_fling_deceleration" /> <java-symbol type="integer" name="config_dockedStackDividerSnapMode" /> <java-symbol type="integer" name="config_pictureInPictureSnapMode" /> <java-symbol type="fraction" name="docked_stack_divider_fixed_ratio" /> @@ -2902,6 +2903,7 @@ <java-symbol type="id" name="autofill_dataset_picker"/> <java-symbol type="id" name="autofill_dataset_list"/> <java-symbol type="id" name="autofill" /> + <java-symbol type="id" name="autofill_save_custom_subtitle" /> <java-symbol type="id" name="autofill_save_title" /> <java-symbol type="id" name="autofill_save_subtitle" /> <java-symbol type="id" name="autofill_save_no" /> diff --git a/libs/hwui/GlopBuilder.cpp b/libs/hwui/GlopBuilder.cpp index 2e9a6e895d8a..c19c1a11e3e2 100644 --- a/libs/hwui/GlopBuilder.cpp +++ b/libs/hwui/GlopBuilder.cpp @@ -457,11 +457,13 @@ GlopBuilder& GlopBuilder::setFillTextureLayer(GlLayer& layer, float alpha) { return *this; } -GlopBuilder& GlopBuilder::setFillExternalTexture(Texture& texture, Matrix4& textureTransform) { +GlopBuilder& GlopBuilder::setFillExternalTexture(Texture& texture, Matrix4& textureTransform, + bool requiresFilter) { TRIGGER_STAGE(kFillStage); REQUIRE_STAGES(kMeshStage | kRoundRectClipStage); - mOutGlop->fill.texture = { &texture, GL_LINEAR, GL_CLAMP_TO_EDGE, &textureTransform }; + GLenum filter = requiresFilter ? GL_LINEAR : GL_NEAREST; + mOutGlop->fill.texture = { &texture, filter, GL_CLAMP_TO_EDGE, &textureTransform }; setFill(SK_ColorWHITE, 1.0f, SkBlendMode::kSrc, Blend::ModeOrderSwap::NoSwap, nullptr, nullptr); diff --git a/libs/hwui/GlopBuilder.h b/libs/hwui/GlopBuilder.h index 87b1568ed72b..6d11da19e138 100644 --- a/libs/hwui/GlopBuilder.h +++ b/libs/hwui/GlopBuilder.h @@ -75,7 +75,8 @@ public: GlopBuilder& setFillTextureLayer(GlLayer& layer, float alpha); // TODO: setFillLayer normally forces its own wrap & filter mode, // which isn't always correct. - GlopBuilder& setFillExternalTexture(Texture& texture, Matrix4& textureTransform); + GlopBuilder& setFillExternalTexture(Texture& texture, Matrix4& textureTransform, + bool requiresFilter); GlopBuilder& setTransform(const Matrix4& canvas, const int transformFlags); diff --git a/libs/hwui/OpenGLReadback.cpp b/libs/hwui/OpenGLReadback.cpp index b073070b58b6..19d5d9d2250e 100644 --- a/libs/hwui/OpenGLReadback.cpp +++ b/libs/hwui/OpenGLReadback.cpp @@ -199,6 +199,7 @@ inline CopyResult copyTextureInto(Caches& caches, RenderState& renderState, GL_TEXTURE_2D, texture, 0); { + bool requiresFilter; // Draw & readback renderState.setViewport(destWidth, destHeight); renderState.scissor().setEnabled(false); @@ -216,12 +217,17 @@ inline CopyResult copyTextureInto(Caches& caches, RenderState& renderState, croppedTexTransform.scale(srcRect.getWidth() / sourceTexture.width(), srcRect.getHeight() / sourceTexture.height(), 1); croppedTexTransform.multiply(sFlipV); + requiresFilter = srcRect.getWidth() != (float) destWidth + || srcRect.getHeight() != (float) destHeight; + } else { + requiresFilter = sourceTexture.width() != (uint32_t) destWidth + || sourceTexture.height() != (uint32_t) destHeight; } Glop glop; GlopBuilder(renderState, caches, &glop) .setRoundRectClipState(nullptr) .setMeshTexturedUnitQuad(nullptr) - .setFillExternalTexture(sourceTexture, croppedTexTransform) + .setFillExternalTexture(sourceTexture, croppedTexTransform, requiresFilter) .setTransform(Matrix4::identity(), TransformFlags::None) .setModelViewMapUnitToRect(Rect(destWidth, destHeight)) .build(); diff --git a/media/java/android/media/MediaCas.java b/media/java/android/media/MediaCas.java index ce50cc801daf..12352e7f8b24 100644 --- a/media/java/android/media/MediaCas.java +++ b/media/java/android/media/MediaCas.java @@ -18,21 +18,20 @@ package android.media; import android.annotation.NonNull; import android.annotation.Nullable; +import android.hardware.cas.V1_0.*; import android.media.MediaCasException.*; import android.os.Handler; import android.os.HandlerThread; -import android.os.IBinder; +import android.os.IHwBinder; import android.os.Looper; import android.os.Message; -import android.os.Parcel; -import android.os.Parcelable; import android.os.Process; import android.os.RemoteException; -import android.os.ServiceManager; -import android.os.ServiceSpecificException; import android.util.Log; import android.util.Singleton; +import java.util.ArrayList; + /** * MediaCas can be used to obtain keys for descrambling protected media streams, in * conjunction with {@link android.media.MediaDescrambler}. The MediaCas APIs are @@ -95,7 +94,6 @@ import android.util.Singleton; */ public final class MediaCas implements AutoCloseable { private static final String TAG = "MediaCas"; - private final ParcelableCasData mCasData = new ParcelableCasData(); private ICas mICas; private EventListener mListener; private HandlerThread mHandlerThread; @@ -105,8 +103,10 @@ public final class MediaCas implements AutoCloseable { new Singleton<IMediaCasService>() { @Override protected IMediaCasService create() { - return IMediaCasService.Stub.asInterface( - ServiceManager.getService("media.cas")); + try { + return IMediaCasService.getService(); + } catch (RemoteException e) {} + return null; } }; @@ -136,14 +136,15 @@ public final class MediaCas implements AutoCloseable { @Override public void handleMessage(Message msg) { if (msg.what == MSG_CAS_EVENT) { - mListener.onEvent(MediaCas.this, msg.arg1, msg.arg2, (byte[]) msg.obj); + mListener.onEvent(MediaCas.this, msg.arg1, msg.arg2, + toBytes((ArrayList<Byte>) msg.obj)); } } } private final ICasListener.Stub mBinder = new ICasListener.Stub() { @Override - public void onEvent(int event, int arg, @Nullable byte[] data) + public void onEvent(int event, int arg, @Nullable ArrayList<Byte> data) throws RemoteException { mEventHandler.sendMessage(mEventHandler.obtainMessage( EventHandler.MSG_CAS_EVENT, event, arg, data)); @@ -151,51 +152,6 @@ public final class MediaCas implements AutoCloseable { }; /** - * Class for parceling byte array data over ICas binder. - */ - static class ParcelableCasData implements Parcelable { - private byte[] mData; - private int mOffset; - private int mLength; - - ParcelableCasData() { - mData = null; - mOffset = mLength = 0; - } - - private ParcelableCasData(Parcel in) { - this(); - } - - void set(@NonNull byte[] data, int offset, int length) { - mData = data; - mOffset = offset; - mLength = length; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeByteArray(mData, mOffset, mLength); - } - - public static final Parcelable.Creator<ParcelableCasData> CREATOR - = new Parcelable.Creator<ParcelableCasData>() { - public ParcelableCasData createFromParcel(Parcel in) { - return new ParcelableCasData(in); - } - - public ParcelableCasData[] newArray(int size) { - return new ParcelableCasData[size]; - } - }; - } - - /** * Describe a CAS plugin with its CA_system_ID and string name. * * Returned as results of {@link #enumeratePlugins}. @@ -210,9 +166,9 @@ public final class MediaCas implements AutoCloseable { mName = null; } - PluginDescriptor(int CA_system_id, String name) { - mCASystemId = CA_system_id; - mName = name; + PluginDescriptor(@NonNull HidlCasPluginDescriptor descriptor) { + mCASystemId = descriptor.caSystemId; + mName = descriptor.name; } public int getSystemId() { @@ -230,13 +186,38 @@ public final class MediaCas implements AutoCloseable { } } + private ArrayList<Byte> toByteArray(@NonNull byte[] data, int offset, int length) { + ArrayList<Byte> byteArray = new ArrayList<Byte>(length); + for (int i = 0; i < length; i++) { + byteArray.add(Byte.valueOf(data[offset + i])); + } + return byteArray; + } + + private ArrayList<Byte> toByteArray(@Nullable byte[] data) { + if (data == null) { + return new ArrayList<Byte>(); + } + return toByteArray(data, 0, data.length); + } + + private byte[] toBytes(@NonNull ArrayList<Byte> byteArray) { + byte[] data = null; + if (byteArray != null) { + data = new byte[byteArray.size()]; + for (int i = 0; i < data.length; i++) { + data[i] = byteArray.get(i); + } + } + return data; + } /** * Class for an open session with the CA system. */ public final class Session implements AutoCloseable { - final byte[] mSessionId; + final ArrayList<Byte> mSessionId; - Session(@NonNull byte[] sessionId) { + Session(@NonNull ArrayList<Byte> sessionId) { mSessionId = sessionId; } @@ -254,9 +235,8 @@ public final class MediaCas implements AutoCloseable { validateInternalStates(); try { - mICas.setSessionPrivateData(mSessionId, data); - } catch (ServiceSpecificException e) { - MediaCasException.throwExceptions(e); + MediaCasException.throwExceptionIfNeeded( + mICas.setSessionPrivateData(mSessionId, toByteArray(data, 0, data.length))); } catch (RemoteException e) { cleanupAndRethrowIllegalState(); } @@ -279,10 +259,8 @@ public final class MediaCas implements AutoCloseable { validateInternalStates(); try { - mCasData.set(data, offset, length); - mICas.processEcm(mSessionId, mCasData); - } catch (ServiceSpecificException e) { - MediaCasException.throwExceptions(e); + MediaCasException.throwExceptionIfNeeded( + mICas.processEcm(mSessionId, toByteArray(data, offset, length))); } catch (RemoteException e) { cleanupAndRethrowIllegalState(); } @@ -314,57 +292,22 @@ public final class MediaCas implements AutoCloseable { validateInternalStates(); try { - mICas.closeSession(mSessionId); - } catch (ServiceSpecificException e) { - MediaCasStateException.throwExceptions(e); + MediaCasStateException.throwExceptionIfNeeded( + mICas.closeSession(mSessionId)); } catch (RemoteException e) { cleanupAndRethrowIllegalState(); } } } - Session createFromSessionId(byte[] sessionId) { - if (sessionId == null || sessionId.length == 0) { + Session createFromSessionId(@NonNull ArrayList<Byte> sessionId) { + if (sessionId == null || sessionId.size() == 0) { return null; } return new Session(sessionId); } /** - * Class for parceling CAS plugin descriptors over IMediaCasService binder. - */ - static class ParcelableCasPluginDescriptor - extends PluginDescriptor implements Parcelable { - - private ParcelableCasPluginDescriptor(int CA_system_id, String name) { - super(CA_system_id, name); - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - Log.w(TAG, "ParcelableCasPluginDescriptor.writeToParcel shouldn't be called!"); - } - - public static final Parcelable.Creator<ParcelableCasPluginDescriptor> CREATOR - = new Parcelable.Creator<ParcelableCasPluginDescriptor>() { - public ParcelableCasPluginDescriptor createFromParcel(Parcel in) { - int CA_system_id = in.readInt(); - String name = in.readString(); - return new ParcelableCasPluginDescriptor(CA_system_id, name); - } - - public ParcelableCasPluginDescriptor[] newArray(int size) { - return new ParcelableCasPluginDescriptor[size]; - } - }; - } - - /** * Query if a certain CA system is supported on this device. * * @param CA_system_id the id of the CA system. @@ -393,13 +336,14 @@ public final class MediaCas implements AutoCloseable { if (service != null) { try { - ParcelableCasPluginDescriptor[] descriptors = service.enumeratePlugins(); - if (descriptors.length == 0) { + ArrayList<HidlCasPluginDescriptor> descriptors = + service.enumeratePlugins(); + if (descriptors.size() == 0) { return null; } - PluginDescriptor[] results = new PluginDescriptor[descriptors.length]; + PluginDescriptor[] results = new PluginDescriptor[descriptors.size()]; for (int i = 0; i < results.length; i++) { - results[i] = descriptors[i]; + results[i] = new PluginDescriptor(descriptors.get(i)); } return results; } catch (RemoteException e) { @@ -430,7 +374,7 @@ public final class MediaCas implements AutoCloseable { } } - IBinder getBinder() { + IHwBinder getBinder() { validateInternalStates(); return mICas.asBinder(); @@ -497,14 +441,22 @@ public final class MediaCas implements AutoCloseable { validateInternalStates(); try { - mICas.setPrivateData(data); - } catch (ServiceSpecificException e) { - MediaCasException.throwExceptions(e); + MediaCasException.throwExceptionIfNeeded( + mICas.setPrivateData(toByteArray(data, 0, data.length))); } catch (RemoteException e) { cleanupAndRethrowIllegalState(); } } + private class OpenSessionCallback implements ICas.openSessionCallback { + public Session mSession; + public int mStatus; + @Override + public void onValues(int status, ArrayList<Byte> sessionId) { + mStatus = status; + mSession = createFromSessionId(sessionId); + } + } /** * Open a session to descramble one or more streams scrambled by the * conditional access system. @@ -519,9 +471,10 @@ public final class MediaCas implements AutoCloseable { validateInternalStates(); try { - return createFromSessionId(mICas.openSession()); - } catch (ServiceSpecificException e) { - MediaCasException.throwExceptions(e); + OpenSessionCallback cb = new OpenSessionCallback(); + mICas.openSession(cb); + MediaCasException.throwExceptionIfNeeded(cb.mStatus); + return cb.mSession; } catch (RemoteException e) { cleanupAndRethrowIllegalState(); } @@ -544,10 +497,8 @@ public final class MediaCas implements AutoCloseable { validateInternalStates(); try { - mCasData.set(data, offset, length); - mICas.processEmm(mCasData); - } catch (ServiceSpecificException e) { - MediaCasException.throwExceptions(e); + MediaCasException.throwExceptionIfNeeded( + mICas.processEmm(toByteArray(data, offset, length))); } catch (RemoteException e) { cleanupAndRethrowIllegalState(); } @@ -585,9 +536,8 @@ public final class MediaCas implements AutoCloseable { validateInternalStates(); try { - mICas.sendEvent(event, arg, data); - } catch (ServiceSpecificException e) { - MediaCasException.throwExceptions(e); + MediaCasException.throwExceptionIfNeeded( + mICas.sendEvent(event, arg, toByteArray(data))); } catch (RemoteException e) { cleanupAndRethrowIllegalState(); } @@ -608,9 +558,8 @@ public final class MediaCas implements AutoCloseable { validateInternalStates(); try { - mICas.provision(provisionString); - } catch (ServiceSpecificException e) { - MediaCasException.throwExceptions(e); + MediaCasException.throwExceptionIfNeeded( + mICas.provision(provisionString)); } catch (RemoteException e) { cleanupAndRethrowIllegalState(); } @@ -631,9 +580,8 @@ public final class MediaCas implements AutoCloseable { validateInternalStates(); try { - mICas.refreshEntitlements(refreshType, refreshData); - } catch (ServiceSpecificException e) { - MediaCasException.throwExceptions(e); + MediaCasException.throwExceptionIfNeeded( + mICas.refreshEntitlements(refreshType, toByteArray(refreshData))); } catch (RemoteException e) { cleanupAndRethrowIllegalState(); } diff --git a/media/java/android/media/MediaCasException.java b/media/java/android/media/MediaCasException.java index 485f6eebf88e..35fb104b7f0b 100644 --- a/media/java/android/media/MediaCasException.java +++ b/media/java/android/media/MediaCasException.java @@ -16,60 +16,29 @@ package android.media; -import android.os.ServiceSpecificException; +import android.hardware.cas.V1_0.Status; /** * Base class for MediaCas exceptions */ public class MediaCasException extends Exception { - - /** @hide */ - public static final int DRM_ERROR_BASE = -2000; - /** @hide */ - public static final int ERROR_DRM_UNKNOWN = DRM_ERROR_BASE; - /** @hide */ - public static final int ERROR_DRM_NO_LICENSE = DRM_ERROR_BASE - 1; - /** @hide */ - public static final int ERROR_DRM_LICENSE_EXPIRED = DRM_ERROR_BASE - 2; - /** @hide */ - public static final int ERROR_DRM_SESSION_NOT_OPENED = DRM_ERROR_BASE - 3; - /** @hide */ - public static final int ERROR_DRM_DECRYPT_UNIT_NOT_INITIALIZED = DRM_ERROR_BASE - 4; - /** @hide */ - public static final int ERROR_DRM_DECRYPT = DRM_ERROR_BASE - 5; - /** @hide */ - public static final int ERROR_DRM_CANNOT_HANDLE = DRM_ERROR_BASE - 6; - /** @hide */ - public static final int ERROR_DRM_TAMPER_DETECTED = DRM_ERROR_BASE - 7; - /** @hide */ - public static final int ERROR_DRM_NOT_PROVISIONED = DRM_ERROR_BASE - 8; - /** @hide */ - public static final int ERROR_DRM_DEVICE_REVOKED = DRM_ERROR_BASE - 9; - /** @hide */ - public static final int ERROR_DRM_RESOURCE_BUSY = DRM_ERROR_BASE - 10; - /** @hide */ - public static final int ERROR_DRM_INSUFFICIENT_OUTPUT_PROTECTION = DRM_ERROR_BASE - 11; - /** @hide */ - public static final int ERROR_DRM_LAST_USED_ERRORCODE = DRM_ERROR_BASE - 11; - /** @hide */ - public static final int ERROR_DRM_VENDOR_MAX = DRM_ERROR_BASE - 500; - /** @hide */ - public static final int ERROR_DRM_VENDOR_MIN = DRM_ERROR_BASE - 999; - - /** @hide */ - public MediaCasException(String detailMessage) { + private MediaCasException(String detailMessage) { super(detailMessage); } - static void throwExceptions(ServiceSpecificException e) throws MediaCasException { - if (e.errorCode == ERROR_DRM_NOT_PROVISIONED) { - throw new NotProvisionedException(e.getMessage()); - } else if (e.errorCode == ERROR_DRM_RESOURCE_BUSY) { - throw new ResourceBusyException(e.getMessage()); - } else if (e.errorCode == ERROR_DRM_DEVICE_REVOKED) { - throw new DeniedByServerException(e.getMessage()); + static void throwExceptionIfNeeded(int error) throws MediaCasException { + if (error == Status.OK) { + return; + } + + if (error == Status.ERROR_CAS_NOT_PROVISIONED) { + throw new NotProvisionedException(null); + } else if (error == Status.ERROR_CAS_RESOURCE_BUSY) { + throw new ResourceBusyException(null); + } else if (error == Status.ERROR_CAS_DEVICE_REVOKED) { + throw new DeniedByServerException(null); } else { - MediaCasStateException.throwExceptions(e); + MediaCasStateException.throwExceptionIfNeeded(error); } } diff --git a/media/java/android/media/MediaCasStateException.java b/media/java/android/media/MediaCasStateException.java index cf05c2975272..26c57923912d 100644 --- a/media/java/android/media/MediaCasStateException.java +++ b/media/java/android/media/MediaCasStateException.java @@ -18,9 +18,8 @@ package android.media; import android.annotation.NonNull; import android.annotation.Nullable; -import android.os.ServiceSpecificException; -import static android.media.MediaCasException.*; +import android.hardware.cas.V1_0.Status; /** * Base class for MediaCas runtime exceptions @@ -29,46 +28,62 @@ public class MediaCasStateException extends IllegalStateException { private final int mErrorCode; private final String mDiagnosticInfo; - /** @hide */ - public MediaCasStateException(int err, @Nullable String msg, @Nullable String diagnosticInfo) { + private MediaCasStateException(int err, @Nullable String msg, @Nullable String diagnosticInfo) { super(msg); mErrorCode = err; mDiagnosticInfo = diagnosticInfo; } - static void throwExceptions(ServiceSpecificException e) { + static void throwExceptionIfNeeded(int err) { + throwExceptionIfNeeded(err, null /* msg */); + } + + static void throwExceptionIfNeeded(int err, @Nullable String msg) { + if (err == Status.OK) { + return; + } + if (err == Status.BAD_VALUE) { + throw new IllegalArgumentException(); + } + String diagnosticInfo = ""; - switch (e.errorCode) { - case ERROR_DRM_UNKNOWN: + switch (err) { + case Status.ERROR_CAS_UNKNOWN: diagnosticInfo = "General CAS error"; break; - case ERROR_DRM_NO_LICENSE: + case Status.ERROR_CAS_NO_LICENSE: diagnosticInfo = "No license"; break; - case ERROR_DRM_LICENSE_EXPIRED: + case Status.ERROR_CAS_LICENSE_EXPIRED: diagnosticInfo = "License expired"; break; - case ERROR_DRM_SESSION_NOT_OPENED: + case Status.ERROR_CAS_SESSION_NOT_OPENED: diagnosticInfo = "Session not opened"; break; - case ERROR_DRM_DECRYPT_UNIT_NOT_INITIALIZED: - diagnosticInfo = "Not initialized"; + case Status.ERROR_CAS_CANNOT_HANDLE: + diagnosticInfo = "Unsupported scheme or data format"; break; - case ERROR_DRM_DECRYPT: - diagnosticInfo = "Decrypt error"; + case Status.ERROR_CAS_INVALID_STATE: + diagnosticInfo = "Invalid CAS state"; break; - case ERROR_DRM_CANNOT_HANDLE: - diagnosticInfo = "Unsupported scheme or data format"; + case Status.ERROR_CAS_INSUFFICIENT_OUTPUT_PROTECTION: + diagnosticInfo = "Insufficient output protection"; break; - case ERROR_DRM_TAMPER_DETECTED: + case Status.ERROR_CAS_TAMPER_DETECTED: diagnosticInfo = "Tamper detected"; break; + case Status.ERROR_CAS_DECRYPT_UNIT_NOT_INITIALIZED: + diagnosticInfo = "Not initialized"; + break; + case Status.ERROR_CAS_DECRYPT: + diagnosticInfo = "Decrypt error"; + break; default: diagnosticInfo = "Unknown CAS state exception"; break; } - throw new MediaCasStateException(e.errorCode, e.getMessage(), - String.format("%s (err=%d)", diagnosticInfo, e.errorCode)); + throw new MediaCasStateException(err, msg, + String.format("%s (err=%d)", diagnosticInfo, err)); } /** diff --git a/media/java/android/media/MediaCodec.java b/media/java/android/media/MediaCodec.java index e667554dab64..3d5f6bc9ac71 100644 --- a/media/java/android/media/MediaCodec.java +++ b/media/java/android/media/MediaCodec.java @@ -26,6 +26,7 @@ import android.media.MediaCodecInfo.CodecCapabilities; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; +import android.os.IHwBinder; import android.os.Looper; import android.os.Message; import android.os.PersistableBundle; @@ -1903,7 +1904,7 @@ final public class MediaCodec { private void configure( @Nullable MediaFormat format, @Nullable Surface surface, - @Nullable MediaCrypto crypto, @Nullable IBinder descramblerBinder, + @Nullable MediaCrypto crypto, @Nullable IHwBinder descramblerBinder, @ConfigureFlag int flags) { if (crypto != null && descramblerBinder != null) { throw new IllegalArgumentException("Can't use crypto and descrambler together!"); @@ -2018,7 +2019,7 @@ final public class MediaCodec { private native final void native_configure( @Nullable String[] keys, @Nullable Object[] values, @Nullable Surface surface, @Nullable MediaCrypto crypto, - @Nullable IBinder descramblerBinder, @ConfigureFlag int flags); + @Nullable IHwBinder descramblerBinder, @ConfigureFlag int flags); /** * Requests a Surface to use as the input to an encoder, in place of input buffers. This diff --git a/media/java/android/media/MediaDescrambler.java b/media/java/android/media/MediaDescrambler.java index b75b7dd8b742..40c837b13cc8 100644 --- a/media/java/android/media/MediaDescrambler.java +++ b/media/java/android/media/MediaDescrambler.java @@ -17,10 +17,9 @@ package android.media; import android.annotation.NonNull; +import android.hardware.cas.V1_0.*; import android.media.MediaCasException.UnsupportedCasException; -import android.os.IBinder; -import android.os.Parcel; -import android.os.Parcelable; +import android.os.IHwBinder; import android.os.RemoteException; import android.os.ServiceSpecificException; import android.util.Log; @@ -40,7 +39,7 @@ import java.nio.ByteBuffer; */ public final class MediaDescrambler implements AutoCloseable { private static final String TAG = "MediaDescrambler"; - private IDescrambler mIDescrambler; + private IDescramblerBase mIDescrambler; private final void validateInternalStates() { if (mIDescrambler == null) { @@ -54,39 +53,6 @@ public final class MediaDescrambler implements AutoCloseable { } /** - * Class for parceling descrambling parameters over IDescrambler binder. - */ - // This class currently is not used by Java binder. descramble() goes through - // jni to use shared memory. However, the parcelable is still required for AIDL. - static class DescrambleInfo implements Parcelable { - private DescrambleInfo() { - } - - private DescrambleInfo(Parcel in) { - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - } - - public static final Parcelable.Creator<DescrambleInfo> CREATOR - = new Parcelable.Creator<DescrambleInfo>() { - public DescrambleInfo createFromParcel(Parcel in) { - return new DescrambleInfo(in); - } - - public DescrambleInfo[] newArray(int size) { - return new DescrambleInfo[size]; - } - }; - } - - /** * Instantiate a MediaDescrambler. * * @param CA_system_id The system id of the scrambling scheme. @@ -107,7 +73,7 @@ public final class MediaDescrambler implements AutoCloseable { native_setup(mIDescrambler.asBinder()); } - IBinder getBinder() { + IHwBinder getBinder() { validateInternalStates(); return mIDescrambler.asBinder(); @@ -151,9 +117,8 @@ public final class MediaDescrambler implements AutoCloseable { validateInternalStates(); try { - mIDescrambler.setMediaCasSession(session.mSessionId); - } catch (ServiceSpecificException e) { - MediaCasStateException.throwExceptions(e); + MediaCasStateException.throwExceptionIfNeeded( + mIDescrambler.setMediaCasSession(session.mSessionId)); } catch (RemoteException e) { cleanupAndRethrowIllegalState(); } @@ -210,7 +175,9 @@ public final class MediaDescrambler implements AutoCloseable { srcBuf, srcBuf.position(), srcBuf.limit(), dstBuf, dstBuf.position(), dstBuf.limit()); } catch (ServiceSpecificException e) { - MediaCasStateException.throwExceptions(e); + MediaCasStateException.throwExceptionIfNeeded(e.errorCode, e.getMessage()); + } catch (RemoteException e) { + cleanupAndRethrowIllegalState(); } return -1; } @@ -234,12 +201,12 @@ public final class MediaDescrambler implements AutoCloseable { } private static native final void native_init(); - private native final void native_setup(@NonNull IBinder decramblerBinder); + private native final void native_setup(@NonNull IHwBinder decramblerBinder); private native final void native_release(); private native final int native_descramble( byte key, int numSubSamples, int[] numBytesOfClearData, int[] numBytesOfEncryptedData, @NonNull ByteBuffer srcBuf, int srcOffset, int srcLimit, - ByteBuffer dstBuf, int dstOffset, int dstLimit); + ByteBuffer dstBuf, int dstOffset, int dstLimit) throws RemoteException; static { System.loadLibrary("media_jni"); diff --git a/media/java/android/media/MediaExtractor.java b/media/java/android/media/MediaExtractor.java index fe461be6dea5..2c1b4b3526e0 100644 --- a/media/java/android/media/MediaExtractor.java +++ b/media/java/android/media/MediaExtractor.java @@ -26,8 +26,8 @@ import android.media.MediaCodec; import android.media.MediaFormat; import android.media.MediaHTTPService; import android.net.Uri; -import android.os.Bundle; import android.os.IBinder; +import android.os.IHwBinder; import android.os.PersistableBundle; import com.android.internal.util.Preconditions; @@ -38,9 +38,8 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.nio.ByteBuffer; import java.nio.ByteOrder; -import java.util.Collections; +import java.util.ArrayList; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.UUID; @@ -263,7 +262,7 @@ final public class MediaExtractor { nativeSetMediaCas(mediaCas.getBinder()); } - private native final void nativeSetMediaCas(@NonNull IBinder casBinder); + private native final void nativeSetMediaCas(@NonNull IHwBinder casBinder); /** * Describes the conditional access system used to scramble a track. @@ -300,6 +299,14 @@ final public class MediaExtractor { } } + private ArrayList<Byte> toByteArray(@NonNull byte[] data) { + ArrayList<Byte> byteArray = new ArrayList<Byte>(data.length); + for (int i = 0; i < data.length; i++) { + byteArray.add(i, Byte.valueOf(data[i])); + } + return byteArray; + } + /** * Retrieves the information about the conditional access system used to scramble * a track. @@ -317,7 +324,7 @@ final public class MediaExtractor { buf.rewind(); final byte[] sessionId = new byte[buf.remaining()]; buf.get(sessionId); - session = mMediaCas.createFromSessionId(sessionId); + session = mMediaCas.createFromSessionId(toByteArray(sessionId)); } return new CasInfo(systemId, session); } diff --git a/media/jni/Android.bp b/media/jni/Android.bp index 9686ab5cafb1..02667ca070a4 100644 --- a/media/jni/Android.bp +++ b/media/jni/Android.bp @@ -50,6 +50,12 @@ cc_library_shared { "libexif", "libpiex", "libandroidfw", + "libhidlbase", + "libhidltransport", + "android.hardware.cas@1.0", + "android.hardware.cas.native@1.0", + "android.hidl.memory@1.0", + "android.hidl.token@1.0-utils", ], header_libs: ["libhardware_headers"], diff --git a/media/jni/android_media_MediaCodec.cpp b/media/jni/android_media_MediaCodec.cpp index 04230a0ab6bd..a27d15782905 100644 --- a/media/jni/android_media_MediaCodec.cpp +++ b/media/jni/android_media_MediaCodec.cpp @@ -21,6 +21,7 @@ #include "android_media_MediaCodec.h" #include "android_media_MediaCrypto.h" +#include "android_media_MediaDescrambler.h" #include "android_media_MediaMetricsJNI.h" #include "android_media_Utils.h" #include "android_runtime/AndroidRuntime.h" @@ -29,7 +30,7 @@ #include "jni.h" #include "JNIHelp.h" -#include <android/media/IDescrambler.h> +#include <android/hardware/cas/native/1.0/IDescrambler.h> #include <cutils/compiler.h> @@ -1010,8 +1011,7 @@ static void android_media_MediaCodec_native_configure( sp<IDescrambler> descrambler; if (descramblerBinderObj != NULL) { - sp<IBinder> binder = ibinderForJavaObject(env, descramblerBinderObj); - descrambler = interface_cast<IDescrambler>(binder); + descrambler = JDescrambler::GetDescrambler(env, descramblerBinderObj); } err = codec->configure(format, bufferProducer, crypto, descrambler, flags); @@ -1952,7 +1952,7 @@ static const JNINativeMethod gMethods[] = { { "native_configure", "([Ljava/lang/String;[Ljava/lang/Object;Landroid/view/Surface;" - "Landroid/media/MediaCrypto;Landroid/os/IBinder;I)V", + "Landroid/media/MediaCrypto;Landroid/os/IHwBinder;I)V", (void *)android_media_MediaCodec_native_configure }, { "native_setSurface", diff --git a/media/jni/android_media_MediaCodec.h b/media/jni/android_media_MediaCodec.h index c9a1700215a4..2ec8703adc83 100644 --- a/media/jni/android_media_MediaCodec.h +++ b/media/jni/android_media_MediaCodec.h @@ -36,10 +36,13 @@ class IGraphicBufferProducer; struct MediaCodec; struct PersistentSurface; class Surface; -namespace media { -class IDescrambler; -}; -using namespace media; +namespace hardware { +namespace cas { +namespace native { +namespace V1_0 { +struct IDescrambler; +}}}} +using hardware::cas::native::V1_0::IDescrambler; struct JMediaCodec : public AHandler { JMediaCodec( diff --git a/media/jni/android_media_MediaDescrambler.cpp b/media/jni/android_media_MediaDescrambler.cpp index 85d33b773007..bee52188e798 100644 --- a/media/jni/android_media_MediaDescrambler.cpp +++ b/media/jni/android_media_MediaDescrambler.cpp @@ -20,16 +20,19 @@ #include "android_media_MediaDescrambler.h" #include "android_runtime/AndroidRuntime.h" -#include "android_util_Binder.h" +#include "android_os_HwRemoteBinder.h" #include "JNIHelp.h" -#include <android/media/IDescrambler.h> +#include <android/hardware/cas/native/1.0/BpHwDescrambler.h> +#include <android/hardware/cas/native/1.0/BnHwDescrambler.h> #include <binder/MemoryDealer.h> +#include <hidl/HidlSupport.h> #include <media/stagefright/foundation/ADebug.h> #include <nativehelper/ScopedLocalRef.h> namespace android { -using media::MediaDescrambler::DescrambleInfo; + +using hardware::hidl_handle; struct fields_t { jfieldID context; @@ -94,10 +97,9 @@ static status_t getBufferAndSize( } JDescrambler::JDescrambler(JNIEnv *env, jobject descramblerBinderObj) { - sp<IDescrambler> cas; - if (descramblerBinderObj != NULL) { - sp<IBinder> binder = ibinderForJavaObject(env, descramblerBinderObj); - mDescrambler = interface_cast<IDescrambler>(binder); + mDescrambler = GetDescrambler(env, descramblerBinderObj); + if (mDescrambler == NULL) { + jniThrowException(env, "java/lang/NullPointerException", NULL); } } @@ -108,9 +110,23 @@ JDescrambler::~JDescrambler() { mDealer.clear(); } -void JDescrambler::ensureBufferCapacity(size_t neededSize) { +// static +sp<IDescrambler> JDescrambler::GetDescrambler(JNIEnv *env, jobject obj) { + if (obj != NULL) { + sp<hardware::IBinder> hwBinder = + JHwRemoteBinder::GetNativeContext(env, obj)->getBinder(); + + if (hwBinder != NULL) { + return hardware::fromBinder< + IDescrambler, BpHwDescrambler, BnHwDescrambler>(hwBinder); + } + } + return NULL; +} + +bool JDescrambler::ensureBufferCapacity(size_t neededSize) { if (mMem != NULL && mMem->size() >= neededSize) { - return; + return true; } ALOGV("ensureBufferCapacity: current size %zu, new size %zu", @@ -122,49 +138,84 @@ void JDescrambler::ensureBufferCapacity(size_t neededSize) { neededSize = (neededSize + 65535) & ~65535; mDealer = new MemoryDealer(neededSize, "JDescrambler"); mMem = mDealer->allocate(neededSize); + + ssize_t offset; + size_t size; + sp<IMemoryHeap> heap = mMem->getMemory(&offset, &size); + if (heap == NULL) { + return false; + } + + native_handle_t* nativeHandle = native_handle_create(1, 0); + if (!nativeHandle) { + ALOGE("ensureBufferCapacity: failed to create native handle"); + return false; + } + nativeHandle->data[0] = heap->getHeapID(); + mDescramblerSrcBuffer.heapBase = hidl_memory("ashmem", + hidl_handle(nativeHandle), heap->getSize()); + mDescramblerSrcBuffer.offset = (uint64_t) offset; + mDescramblerSrcBuffer.size = (uint64_t) size; + return true; } -Status JDescrambler::descramble( +status_t JDescrambler::descramble( jbyte key, - size_t numSubSamples, ssize_t totalLength, - DescramblerPlugin::SubSample *subSamples, + const hidl_vec<SubSample>& subSamples, const void *srcPtr, jint srcOffset, void *dstPtr, jint dstOffset, - ssize_t *result) { + Status *status, + uint32_t *bytesWritten, + hidl_string *detailedError) { // TODO: IDescrambler::descramble() is re-entrant, however because we // only have 1 shared mem buffer, we can only do 1 descramble at a time. // Concurrency might be improved by allowing on-demand allocation of up // to 2 shared mem buffers. Mutex::Autolock autolock(mSharedMemLock); - ensureBufferCapacity(totalLength); + if (!ensureBufferCapacity(totalLength)) { + return NO_MEMORY; + } memcpy(mMem->pointer(), (const void*)((const uint8_t*)srcPtr + srcOffset), totalLength); - DescrambleInfo info; - info.dstType = DescrambleInfo::kDestinationTypeVmPointer; - info.numSubSamples = numSubSamples; - info.scramblingControl = (DescramblerPlugin::ScramblingControl) key; - info.subSamples = subSamples; - info.srcMem = mMem; - info.srcOffset = 0; - info.dstPtr = NULL; - info.dstOffset = 0; - - int32_t descrambleResult; - Status status = mDescrambler->descramble(info, &descrambleResult); - - if (status.isOk()) { - *result = (descrambleResult <= totalLength) ? descrambleResult : -1; - if (*result > 0) { - memcpy((void*)((uint8_t*)dstPtr + dstOffset), mMem->pointer(), *result); + DestinationBuffer dstBuffer; + dstBuffer.type = BufferType::SHARED_MEMORY; + dstBuffer.nonsecureMemory = mDescramblerSrcBuffer; + + auto err = mDescrambler->descramble( + (ScramblingControl) key, + subSamples, + mDescramblerSrcBuffer, + 0, + dstBuffer, + 0, + [&status, &bytesWritten, &detailedError] ( + Status _status, uint32_t _bytesWritten, + const hidl_string& _detailedError) { + *status = _status; + *bytesWritten = _bytesWritten; + *detailedError = _detailedError; + }); + + if (!err.isOk()) { + return FAILED_TRANSACTION; + } + + if (*status == Status::OK) { + if (*bytesWritten > 0 && (ssize_t) *bytesWritten <= totalLength) { + memcpy((void*)((uint8_t*)dstPtr + dstOffset), mMem->pointer(), *bytesWritten); + } else { + // status seems OK but bytesWritten is invalid, we really + // have no idea what is wrong. + *status = Status::ERROR_CAS_UNKNOWN; } } - return status; + return OK; } } // namespace android @@ -191,10 +242,10 @@ static void android_media_MediaDescrambler_native_setup( static ssize_t getSubSampleInfo(JNIEnv *env, jint numSubSamples, jintArray numBytesOfClearDataObj, jintArray numBytesOfEncryptedDataObj, - DescramblerPlugin::SubSample **outSubSamples) { + hidl_vec<SubSample> *outSubSamples) { - if (numSubSamples <= 0 || numSubSamples >= - (signed)(INT32_MAX / sizeof(DescramblerPlugin::SubSample)) ) { + if (numSubSamples <= 0 || + numSubSamples >= (signed)(INT32_MAX / sizeof(SubSample))) { // subSamples array may silently overflow if number of samples are // too large. Use INT32_MAX as maximum allocation size may be less // than SIZE_MAX on some platforms. @@ -215,24 +266,23 @@ static ssize_t getSubSampleInfo(JNIEnv *env, jint numSubSamples, ? NULL : env->GetIntArrayElements(numBytesOfEncryptedDataObj, &isCopy); - DescramblerPlugin::SubSample *subSamples = - new(std::nothrow) DescramblerPlugin::SubSample[numSubSamples]; - + outSubSamples->resize(numSubSamples); + SubSample *subSamples = outSubSamples->data(); if (subSamples == NULL) { ALOGE("Failed to allocate SubSample array!"); return -1; } for (jint i = 0; i < numSubSamples; ++i) { - subSamples[i].mNumBytesOfClearData = + subSamples[i].numBytesOfClearData = (numBytesOfClearData == NULL) ? 0 : numBytesOfClearData[i]; - subSamples[i].mNumBytesOfEncryptedData = + subSamples[i].numBytesOfEncryptedData = (numBytesOfEncryptedData == NULL) ? 0 : numBytesOfEncryptedData[i]; - totalSize += subSamples[i].mNumBytesOfClearData + - subSamples[i].mNumBytesOfEncryptedData; + totalSize += subSamples[i].numBytesOfClearData + + subSamples[i].numBytesOfEncryptedData; } if (numBytesOfEncryptedData != NULL) { @@ -248,12 +298,9 @@ static ssize_t getSubSampleInfo(JNIEnv *env, jint numSubSamples, } if (totalSize < 0) { - delete[] subSamples; return -1; } - *outSubSamples = subSamples; - return totalSize; } @@ -280,12 +327,6 @@ static jthrowable createServiceSpecificException( clazz.get(), ctor, serviceSpecificError, msgObj.get()); } -static void throwServiceSpecificException( - JNIEnv *env, int serviceSpecificError, const char *msg) { - jthrowable exception = createServiceSpecificException(env, serviceSpecificError, msg); - env->Throw(exception); -} - static jint android_media_MediaDescrambler_native_descramble( JNIEnv *env, jobject thiz, jbyte key, jint numSubSamples, jintArray numBytesOfClearDataObj, jintArray numBytesOfEncryptedDataObj, @@ -293,11 +334,12 @@ static jint android_media_MediaDescrambler_native_descramble( jobject dstBuf, jint dstOffset, jint dstLimit) { sp<JDescrambler> descrambler = getDescrambler(env, thiz); if (descrambler == NULL) { - jniThrowException(env, "java/lang/IllegalStateException", NULL); + jniThrowException(env, "java/lang/IllegalStateException", + "Invalid descrambler object!"); return -1; } - DescramblerPlugin::SubSample *subSamples = NULL; + hidl_vec<SubSample> subSamples; ssize_t totalLength = getSubSampleInfo( env, numSubSamples, numBytesOfClearDataObj, numBytesOfEncryptedDataObj, &subSamples); @@ -307,7 +349,6 @@ static jint android_media_MediaDescrambler_native_descramble( return -1; } - ssize_t result = -1; void *srcPtr = NULL, *dstPtr = NULL; jbyteArray srcArray = NULL, dstArray = NULL; status_t err = getBufferAndSize( @@ -329,13 +370,15 @@ static jint android_media_MediaDescrambler_native_descramble( } Status status; - if (err == OK) { - status = descrambler->descramble( - key, numSubSamples, totalLength, subSamples, - srcPtr, srcOffset, dstPtr, dstOffset, &result); - } + uint32_t bytesWritten; + hidl_string detailedError; + + err = descrambler->descramble( + key, totalLength, subSamples, + srcPtr, srcOffset, dstPtr, dstOffset, + &status, &bytesWritten, &detailedError); - delete[] subSamples; + // Release byte array before throwing if (srcArray != NULL) { env->ReleaseByteArrayElements(srcArray, (jbyte *)srcPtr, 0); } @@ -343,51 +386,17 @@ static jint android_media_MediaDescrambler_native_descramble( env->ReleaseByteArrayElements(dstArray, (jbyte *)dstPtr, 0); } - if (!status.isOk()) { - switch (status.exceptionCode()) { - case Status::EX_SECURITY: - jniThrowException(env, "java/lang/SecurityException", - status.exceptionMessage()); - break; - case Status::EX_BAD_PARCELABLE: - jniThrowException(env, "java/lang/BadParcelableException", - status.exceptionMessage()); - break; - case Status::EX_ILLEGAL_ARGUMENT: - jniThrowException(env, "java/lang/IllegalArgumentException", - status.exceptionMessage()); - break; - case Status::EX_NULL_POINTER: - jniThrowException(env, "java/lang/NullPointerException", - status.exceptionMessage()); - break; - case Status::EX_ILLEGAL_STATE: - jniThrowException(env, "java/lang/IllegalStateException", - status.exceptionMessage()); - break; - case Status::EX_NETWORK_MAIN_THREAD: - jniThrowException(env, "java/lang/NetworkOnMainThreadException", - status.exceptionMessage()); - break; - case Status::EX_UNSUPPORTED_OPERATION: - jniThrowException(env, "java/lang/UnsupportedOperationException", - status.exceptionMessage()); - break; - case Status::EX_SERVICE_SPECIFIC: - throwServiceSpecificException(env, status.serviceSpecificErrorCode(), - status.exceptionMessage()); - break; - default: - { - String8 msg; - msg.appendFormat("Unknown exception code: %d, msg: %s", - status.exceptionCode(), status.exceptionMessage().string()); - jniThrowException(env, "java/lang/RuntimeException", msg.string()); - break; - } - } + if (err == NO_MEMORY) { + jniThrowException(env, "java/lang/OutOfMemoryError", NULL); + } else if (err == FAILED_TRANSACTION) { + jniThrowException(env, "android/os/RemoteException", NULL); + } else if (status != Status::OK) { + // Throw ServiceSpecific with cas error code and detailed msg, + // which will be re-thrown as MediaCasStateException. + env->Throw(createServiceSpecificException( + env, (int) status, detailedError.c_str())); } - return result; + return bytesWritten; } static const JNINativeMethod gMethods[] = { @@ -395,7 +404,7 @@ static const JNINativeMethod gMethods[] = { (void *)android_media_MediaDescrambler_native_release }, { "native_init", "()V", (void *)android_media_MediaDescrambler_native_init }, - { "native_setup", "(Landroid/os/IBinder;)V", + { "native_setup", "(Landroid/os/IHwBinder;)V", (void *)android_media_MediaDescrambler_native_setup }, { "native_descramble", "(BI[I[ILjava/nio/ByteBuffer;IILjava/nio/ByteBuffer;II)I", (void *)android_media_MediaDescrambler_native_descramble }, diff --git a/media/jni/android_media_MediaDescrambler.h b/media/jni/android_media_MediaDescrambler.h index aeef05e7968d..015fad22e13b 100644 --- a/media/jni/android_media_MediaDescrambler.h +++ b/media/jni/android_media_MediaDescrambler.h @@ -19,34 +19,37 @@ #include "jni.h" -#include <binder/Status.h> -#include <media/cas/DescramblerAPI.h> +#include <android/hardware/cas/native/1.0/IDescrambler.h> + #include <media/stagefright/foundation/ABase.h> #include <utils/Mutex.h> -#include <utils/RefBase.h> namespace android { class IMemory; class MemoryDealer; -namespace media { -class IDescrambler; -}; -using namespace media; -using binder::Status; + +using hardware::hidl_memory; +using hardware::hidl_string; +using hardware::hidl_vec; +using namespace hardware::cas::V1_0; +using namespace hardware::cas::native::V1_0; struct JDescrambler : public RefBase { JDescrambler(JNIEnv *env, jobject descramberBinderObj); - Status descramble( + status_t descramble( jbyte key, - size_t numSubSamples, ssize_t totalLength, - DescramblerPlugin::SubSample *subSamples, + const hidl_vec<SubSample>& subSamples, const void *srcPtr, jint srcOffset, void *dstPtr, jint dstOffset, - ssize_t *result); + Status *status, + uint32_t *bytesWritten, + hidl_string *detailedError); + + static sp<IDescrambler> GetDescrambler(JNIEnv *env, jobject obj); protected: virtual ~JDescrambler(); @@ -55,9 +58,11 @@ private: sp<IDescrambler> mDescrambler; sp<IMemory> mMem; sp<MemoryDealer> mDealer; + SharedBuffer mDescramblerSrcBuffer; + Mutex mSharedMemLock; - void ensureBufferCapacity(size_t neededSize); + bool ensureBufferCapacity(size_t neededSize); DISALLOW_EVIL_CONSTRUCTORS(JDescrambler); }; diff --git a/media/jni/android_media_MediaExtractor.cpp b/media/jni/android_media_MediaExtractor.cpp index 9e5d3d12f0bd..c9657b119bdb 100644 --- a/media/jni/android_media_MediaExtractor.cpp +++ b/media/jni/android_media_MediaExtractor.cpp @@ -18,16 +18,20 @@ #define LOG_TAG "MediaExtractor-JNI" #include <utils/Log.h> +#include "android_media_MediaDataSource.h" #include "android_media_MediaExtractor.h" #include "android_media_MediaMetricsJNI.h" - #include "android_media_Utils.h" +#include "android_os_HwRemoteBinder.h" #include "android_runtime/AndroidRuntime.h" #include "android_runtime/Log.h" +#include "android_util_Binder.h" #include "jni.h" #include "JNIHelp.h" -#include "android_media_MediaDataSource.h" +#include <android/hardware/cas/1.0/BpHwCas.h> +#include <android/hardware/cas/1.0/BnHwCas.h> +#include <hidl/HybridInterface.h> #include <media/IMediaHTTPService.h> #include <media/hardware/CryptoAPI.h> #include <media/stagefright/foundation/ABuffer.h> @@ -37,14 +41,12 @@ #include <media/stagefright/MediaErrors.h> #include <media/stagefright/MetaData.h> #include <media/stagefright/NuMediaExtractor.h> -#include <android/media/ICas.h> - #include <nativehelper/ScopedLocalRef.h> -#include "android_util_Binder.h" - namespace android { +using namespace hardware::cas::V1_0; + struct fields_t { jfieldID context; @@ -89,8 +91,28 @@ status_t JMediaExtractor::setDataSource(const sp<DataSource> &datasource) { return mImpl->setDataSource(datasource); } -status_t JMediaExtractor::setMediaCas(const sp<ICas> &cas) { - return mImpl->setMediaCas(cas); +status_t JMediaExtractor::setMediaCas(JNIEnv *env, jobject casBinderObj) { + if (casBinderObj == NULL) { + return BAD_VALUE; + } + + sp<hardware::IBinder> hwBinder = + JHwRemoteBinder::GetNativeContext(env, casBinderObj)->getBinder(); + if (hwBinder == NULL) { + return BAD_VALUE; + } + + sp<ICas> cas = hardware::fromBinder<ICas, BpHwCas, BnHwCas>(hwBinder); + if (cas == NULL) { + return BAD_VALUE; + } + + HalToken halToken; + if (!createHalToken(cas, &halToken)) { + return BAD_VALUE; + } + + return mImpl->setMediaCas(halToken); } size_t JMediaExtractor::countTracks() const { @@ -748,23 +770,13 @@ static void android_media_MediaExtractor_setMediaCas( return; } - if (casBinderObj == NULL) { - jniThrowException(env, "java/lang/IllegalArgumentException", NULL); - return; - } - - sp<ICas> cas; - if (casBinderObj != NULL) { - sp<IBinder> binder = ibinderForJavaObject(env, casBinderObj); - cas = interface_cast<ICas>(binder); - } - status_t err = extractor->setMediaCas(cas); + status_t err = extractor->setMediaCas(env, casBinderObj); if (err != OK) { - cas.clear(); + extractor.clear(); jniThrowException( env, - "java/io/IllegalArgumentException", + "java/lang/IllegalArgumentException", "Failed to set MediaCas on extractor."); } } @@ -896,7 +908,7 @@ static const JNINativeMethod gMethods[] = { { "setDataSource", "(Landroid/media/MediaDataSource;)V", (void *)android_media_MediaExtractor_setDataSourceCallback }, - { "nativeSetMediaCas", "(Landroid/os/IBinder;)V", + { "nativeSetMediaCas", "(Landroid/os/IHwBinder;)V", (void *)android_media_MediaExtractor_setMediaCas }, { "getCachedDuration", "()J", diff --git a/media/jni/android_media_MediaExtractor.h b/media/jni/android_media_MediaExtractor.h index 3d8c50b809ac..94d36f25ae1c 100644 --- a/media/jni/android_media_MediaExtractor.h +++ b/media/jni/android_media_MediaExtractor.h @@ -28,10 +28,6 @@ #include "jni.h" namespace android { -namespace media { -class ICas; -}; -using namespace media; struct IMediaHTTPService; class MetaData; @@ -48,7 +44,7 @@ struct JMediaExtractor : public RefBase { status_t setDataSource(int fd, off64_t offset, off64_t size); status_t setDataSource(const sp<DataSource> &source); - status_t setMediaCas(const sp<ICas> &cas); + status_t setMediaCas(JNIEnv *env, jobject casBinderObj); size_t countTracks() const; status_t getTrackFormat(size_t index, jobject *format) const; diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceDiscoveryService.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceDiscoveryService.java index 3b29a6cd7b6c..1e262314284d 100644 --- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceDiscoveryService.java +++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceDiscoveryService.java @@ -77,7 +77,7 @@ public class DeviceDiscoveryService extends Service { private BluetoothAdapter mBluetoothAdapter; private WifiManager mWifiManager; - private BluetoothLeScanner mBLEScanner; + @Nullable private BluetoothLeScanner mBLEScanner; private ScanSettings mDefaultScanSettings = new ScanSettings.Builder().build(); private List<DeviceFilter<?>> mFilters; @@ -185,7 +185,7 @@ public class DeviceDiscoveryService extends Service { mBluetoothAdapter.startDiscovery(); } - if (shouldScan(mBLEFilters)) { + if (shouldScan(mBLEFilters) && mBLEScanner != null) { mBLEScanCallback = new BLEScanCallback(); mBLEScanner.startScan(mBLEScanFilters, mDefaultScanSettings, mBLEScanCallback); } @@ -224,7 +224,7 @@ public class DeviceDiscoveryService extends Service { unregisterReceiver(mBluetoothBroadcastReceiver); mBluetoothBroadcastReceiver = null; } - mBLEScanner.stopScan(mBLEScanCallback); + if (mBLEScanner != null) mBLEScanner.stopScan(mBLEScanCallback); if (mWifiBroadcastReceiver != null) { unregisterReceiver(mWifiBroadcastReceiver); mWifiBroadcastReceiver = null; diff --git a/packages/SettingsLib/src/com/android/settingslib/TronUtils.java b/packages/SettingsLib/src/com/android/settingslib/TronUtils.java index bea6e8f77dd2..945cb5797e8f 100644 --- a/packages/SettingsLib/src/com/android/settingslib/TronUtils.java +++ b/packages/SettingsLib/src/com/android/settingslib/TronUtils.java @@ -16,48 +16,18 @@ package com.android.settingslib; import android.content.Context; -import android.net.NetworkBadging; import com.android.internal.logging.MetricsLogger; +import com.android.settingslib.wifi.AccessPoint.Speed; /** Utilites for Tron Logging. */ public final class TronUtils { - private TronUtils() {}; - - public static void logWifiSettingsBadge(Context context, int badgeEnum) { - logNetworkBadgeMetric(context, "settings_wifibadging", badgeEnum); - } + private static final String TAG = "TronUtils"; - /** - * Logs an occurrence of the given network badge to a Histogram. - * - * @param context Context - * @param histogram the Tron histogram name to write to - * @param badgeEnum the {@link NetworkBadging.Badging} badge value - * @throws IllegalArgumentException if the given badge enum is not supported - */ - private static void logNetworkBadgeMetric( - Context context, String histogram, int badgeEnum) - throws IllegalArgumentException { - int bucket; - switch (badgeEnum) { - case NetworkBadging.BADGING_NONE: - bucket = 0; - break; - case NetworkBadging.BADGING_SD: - bucket = 1; - break; - case NetworkBadging.BADGING_HD: - bucket = 2; - break; - case NetworkBadging.BADGING_4K: - bucket = 3; - break; - default: - throw new IllegalArgumentException("Unsupported badge enum: " + badgeEnum); - } + private TronUtils() {}; - MetricsLogger.histogram(context, histogram, bucket); + public static void logWifiSettingsSpeed(Context context, @Speed int speedEnum) { + MetricsLogger.histogram(context, "settings_wifi_speed_labels", speedEnum); } } diff --git a/packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionParser.java b/packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionParser.java index 167ffe61e42b..fa41c8349540 100644 --- a/packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionParser.java +++ b/packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionParser.java @@ -91,12 +91,10 @@ public class SuggestionParser { // Shared prefs keys for storing dismissed state. // Index into current dismissed state. + public static final String SETUP_TIME = "_setup_time"; private static final String DISMISS_INDEX = "_dismiss_index"; - private static final String SETUP_TIME = "_setup_time"; private static final String IS_DISMISSED = "_is_dismissed"; - private static final long MILLIS_IN_DAY = 24 * 60 * 60 * 1000; - // Default dismiss control for smart suggestions. private static final String DEFAULT_SMART_DISMISS_CONTROL = "0,10"; @@ -386,7 +384,7 @@ public class SuggestionParser { } private long getEndTime(long startTime, int daysDelay) { - long days = daysDelay * MILLIS_IN_DAY; + long days = daysDelay * DateUtils.DAY_IN_MILLIS; return startTime + days; } diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/AccessPoint.java b/packages/SettingsLib/src/com/android/settingslib/wifi/AccessPoint.java index d45ed1922aa4..fc6a1611e697 100644 --- a/packages/SettingsLib/src/com/android/settingslib/wifi/AccessPoint.java +++ b/packages/SettingsLib/src/com/android/settingslib/wifi/AccessPoint.java @@ -16,6 +16,7 @@ package com.android.settingslib.wifi; +import android.annotation.IntDef; import android.annotation.Nullable; import android.app.AppGlobals; import android.content.Context; @@ -53,6 +54,8 @@ import android.util.Log; import com.android.internal.annotations.VisibleForTesting; import com.android.settingslib.R; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Iterator; import java.util.concurrent.ConcurrentHashMap; @@ -82,35 +85,30 @@ public class AccessPoint implements Comparable<AccessPoint> { */ public static final int HIGHER_FREQ_5GHZ = 5900; - /** - * Constant value representing an unlabeled / unscored network. - */ - @VisibleForTesting - static final int SPEED_NONE = 0; - - /** - * Constant value representing a slow speed network connection. - */ - @VisibleForTesting - static final int SPEED_SLOW = 5; - - /** - * Constant value representing a medium speed network connection. - */ - @VisibleForTesting - static final int SPEED_MEDIUM = 10; - - /** - * Constant value representing a fast speed network connection. - */ - @VisibleForTesting - static final int SPEED_FAST = 20; - - /** - * Constant value representing a very fast speed network connection. - */ - @VisibleForTesting - static final int SPEED_VERY_FAST = 30; + @IntDef({Speed.NONE, Speed.SLOW, Speed.MODERATE, Speed.FAST, Speed.VERY_FAST}) + @Retention(RetentionPolicy.SOURCE) + public @interface Speed { + /** + * Constant value representing an unlabeled / unscored network. + */ + int NONE = 0; + /** + * Constant value representing a slow speed network connection. + */ + int SLOW = 5; + /** + * Constant value representing a medium speed network connection. + */ + int MODERATE = 10; + /** + * Constant value representing a fast speed network connection. + */ + int FAST = 20; + /** + * Constant value representing a very fast speed network connection. + */ + int VERY_FAST = 30; + } /** * Experimental: we should be able to show the user the list of BSSIDs and bands @@ -177,7 +175,7 @@ public class AccessPoint implements Comparable<AccessPoint> { private Object mTag; private int mRankingScore = Integer.MIN_VALUE; - private int mSpeed = AccessPoint.SPEED_NONE; + private int mSpeed = Speed.NONE; private boolean mIsScoredNetworkMetered = false; // used to co-relate internal vs returned accesspoint. @@ -322,8 +320,15 @@ public class AccessPoint implements Comparable<AccessPoint> { if (difference != 0) { return difference; } + // Sort by ssid. - return getSsidStr().compareToIgnoreCase(other.getSsidStr()); + difference = getSsidStr().compareToIgnoreCase(other.getSsidStr()); + if (difference != 0) { + return difference; + } + + // Do a case sensitive comparison to distinguish SSIDs that differ in case only + return getSsidStr().compareTo(other.getSsidStr()); } @Override @@ -368,7 +373,7 @@ public class AccessPoint implements Comparable<AccessPoint> { if (mRankingScore != Integer.MIN_VALUE) { builder.append(",rankingScore=").append(mRankingScore); } - if (mSpeed != SPEED_NONE) { + if (mSpeed != Speed.NONE) { builder.append(",speed=").append(mSpeed); } builder.append(",metered=").append(isMetered()); @@ -399,7 +404,7 @@ public class AccessPoint implements Comparable<AccessPoint> { private boolean updateScores(WifiNetworkScoreCache scoreCache) { int oldSpeed = mSpeed; int oldRankingScore = mRankingScore; - mSpeed = SPEED_NONE; + mSpeed = Speed.NONE; mRankingScore = Integer.MIN_VALUE; for (ScanResult result : mScanResultCache.values()) { @@ -675,7 +680,7 @@ public class AccessPoint implements Comparable<AccessPoint> { // TODO(b/62354743): Standardize and international delimiter usage final String concatenator = " / "; - if (mSpeed != SPEED_NONE) { + if (mSpeed != Speed.NONE) { summary.append(getSpeedLabel() + concatenator); } @@ -799,7 +804,7 @@ public class AccessPoint implements Comparable<AccessPoint> { if (mRankingScore != Integer.MIN_VALUE) { visibility.append(" rankingScore=").append(getRankingScore()); } - if (mSpeed != SPEED_NONE) { + if (mSpeed != Speed.NONE) { visibility.append(" speed=").append(getSpeedLabel()); } visibility.append(String.format(" tx=%.1f,", mInfo.txSuccessRate)); @@ -1104,15 +1109,15 @@ public class AccessPoint implements Comparable<AccessPoint> { @Nullable String getSpeedLabel() { switch (mSpeed) { - case SPEED_VERY_FAST: + case Speed.VERY_FAST: return mContext.getString(R.string.speed_label_very_fast); - case SPEED_FAST: + case Speed.FAST: return mContext.getString(R.string.speed_label_fast); - case SPEED_MEDIUM: + case Speed.MODERATE: return mContext.getString(R.string.speed_label_okay); - case SPEED_SLOW: + case Speed.SLOW: return mContext.getString(R.string.speed_label_slow); - case SPEED_NONE: + case Speed.NONE: default: return null; } diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/AccessPointPreference.java b/packages/SettingsLib/src/com/android/settingslib/wifi/AccessPointPreference.java index e7525f3edbc4..e5977f313091 100644 --- a/packages/SettingsLib/src/com/android/settingslib/wifi/AccessPointPreference.java +++ b/packages/SettingsLib/src/com/android/settingslib/wifi/AccessPointPreference.java @@ -161,7 +161,7 @@ public class AccessPointPreference extends Preference { safeSetDefaultIcon(); return; } - TronUtils.logWifiSettingsBadge(context, mWifiSpeed); + TronUtils.logWifiSettingsSpeed(context, mWifiSpeed); // TODO(b/62355275): Revert this to N code after deleting NetworkBadging API Drawable drawable = NetworkBadging.getWifiIcon( diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiTracker.java b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiTracker.java index 9083d90b9085..85a453d24e6f 100644 --- a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiTracker.java +++ b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiTracker.java @@ -91,7 +91,7 @@ public class WifiTracker { private final boolean mIncludeScans; private final boolean mIncludePasspoints; @VisibleForTesting final MainHandler mMainHandler; - private final WorkHandler mWorkHandler; + @VisibleForTesting final WorkHandler mWorkHandler; private WifiTrackerNetworkCallback mNetworkCallback; @@ -653,6 +653,7 @@ public class WifiTracker { /* sticky broadcasts can call this when wifi is disabled */ if (!mWifiManager.isWifiEnabled()) { mMainHandler.sendEmptyMessage(MainHandler.MSG_PAUSE_SCANNING); + clearAccessPointsAndConditionallyUpdate(); return; } @@ -696,6 +697,17 @@ public class WifiTracker { } } + private void clearAccessPointsAndConditionallyUpdate() { + synchronized (mLock) { + if (!mInternalAccessPoints.isEmpty()) { + mInternalAccessPoints.clear(); + if (!mMainHandler.hasMessages(MainHandler.MSG_ACCESS_POINT_CHANGED)) { + mMainHandler.sendEmptyMessage(MainHandler.MSG_ACCESS_POINT_CHANGED); + } + } + } + } + /** * Update all the internal access points rankingScores, badge and metering. * @@ -720,6 +732,9 @@ public class WifiTracker { private void updateWifiState(int state) { mWorkHandler.obtainMessage(WorkHandler.MSG_UPDATE_WIFI_STATE, state, 0).sendToTarget(); + if (!mWifiManager.isWifiEnabled()) { + clearAccessPointsAndConditionallyUpdate(); + } } public static List<AccessPoint> getCurrentAccessPoints(Context context, boolean includeSaved, @@ -803,8 +818,15 @@ public class WifiTracker { mListener.onWifiStateChanged(msg.arg1); break; case MSG_ACCESS_POINT_CHANGED: - copyAndNotifyListeners(true /*notifyListeners*/); - mListener.onAccessPointsChanged(); + // Only notify listeners of changes if we have fresh scan results, otherwise the + // UI will be updated with stale results. We want to copy the APs regardless, + // for instances where forceUpdate was invoked by the caller. + if (mStaleScanResults) { + copyAndNotifyListeners(false /*notifyListeners*/); + } else { + copyAndNotifyListeners(true /*notifyListeners*/); + mListener.onAccessPointsChanged(); + } break; case MSG_RESUME_SCANNING: if (mScanner != null) { @@ -828,7 +850,8 @@ public class WifiTracker { } } - private final class WorkHandler extends Handler { + @VisibleForTesting + final class WorkHandler extends Handler { private static final int MSG_UPDATE_ACCESS_POINTS = 0; private static final int MSG_UPDATE_NETWORK_INFO = 1; private static final int MSG_RESUME = 2; diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/wifi/AccessPointTest.java b/packages/SettingsLib/tests/integ/src/com/android/settingslib/wifi/AccessPointTest.java index 89328ee47f1c..57ad093422a4 100644 --- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/wifi/AccessPointTest.java +++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/wifi/AccessPointTest.java @@ -169,6 +169,21 @@ public class AccessPointTest { } @Test + public void testCompareTo_GivesSsidCasePrecendenceAfterAlphabetical() { + + final String firstName = "aaAaaa"; + final String secondName = "aaaaaa"; + final String thirdName = "BBBBBB"; + + AccessPoint firstAp = new TestAccessPointBuilder(mContext).setSsid(firstName).build(); + AccessPoint secondAp = new TestAccessPointBuilder(mContext).setSsid(secondName).build(); + AccessPoint thirdAp = new TestAccessPointBuilder(mContext).setSsid(thirdName).build(); + + assertSortingWorks(firstAp, secondAp); + assertSortingWorks(secondAp, thirdAp); + } + + @Test public void testCompareTo_AllSortingRulesCombined() { AccessPoint active = new TestAccessPointBuilder(mContext).setActive(true).build(); @@ -334,11 +349,11 @@ public class AccessPointTest { when(mockWifiNetworkScoreCache.getScoredNetwork(any(ScanResult.class))) .thenReturn(buildScoredNetworkWithMockBadgeCurve()); - when(mockBadgeCurve.lookupScore(anyInt())).thenReturn((byte) AccessPoint.SPEED_VERY_FAST); + when(mockBadgeCurve.lookupScore(anyInt())).thenReturn((byte) AccessPoint.Speed.VERY_FAST); ap.update(mockWifiNetworkScoreCache, true /* scoringUiEnabled */); - assertThat(ap.getSpeed()).isEqualTo(AccessPoint.SPEED_VERY_FAST); + assertThat(ap.getSpeed()).isEqualTo(AccessPoint.Speed.VERY_FAST); assertThat(ap.getSpeedLabel()) .isEqualTo(mContext.getString(R.string.speed_label_very_fast)); } @@ -349,11 +364,11 @@ public class AccessPointTest { when(mockWifiNetworkScoreCache.getScoredNetwork(any(ScanResult.class))) .thenReturn(buildScoredNetworkWithMockBadgeCurve()); - when(mockBadgeCurve.lookupScore(anyInt())).thenReturn((byte) AccessPoint.SPEED_FAST); + when(mockBadgeCurve.lookupScore(anyInt())).thenReturn((byte) AccessPoint.Speed.FAST); ap.update(mockWifiNetworkScoreCache, true /* scoringUiEnabled */); - assertThat(ap.getSpeed()).isEqualTo(AccessPoint.SPEED_FAST); + assertThat(ap.getSpeed()).isEqualTo(AccessPoint.Speed.FAST); assertThat(ap.getSpeedLabel()) .isEqualTo(mContext.getString(R.string.speed_label_fast)); } @@ -364,11 +379,11 @@ public class AccessPointTest { when(mockWifiNetworkScoreCache.getScoredNetwork(any(ScanResult.class))) .thenReturn(buildScoredNetworkWithMockBadgeCurve()); - when(mockBadgeCurve.lookupScore(anyInt())).thenReturn((byte) AccessPoint.SPEED_MEDIUM); + when(mockBadgeCurve.lookupScore(anyInt())).thenReturn((byte) AccessPoint.Speed.MODERATE); ap.update(mockWifiNetworkScoreCache, true /* scoringUiEnabled */); - assertThat(ap.getSpeed()).isEqualTo(AccessPoint.SPEED_MEDIUM); + assertThat(ap.getSpeed()).isEqualTo(AccessPoint.Speed.MODERATE); assertThat(ap.getSpeedLabel()) .isEqualTo(mContext.getString(R.string.speed_label_okay)); } @@ -379,11 +394,11 @@ public class AccessPointTest { when(mockWifiNetworkScoreCache.getScoredNetwork(any(ScanResult.class))) .thenReturn(buildScoredNetworkWithMockBadgeCurve()); - when(mockBadgeCurve.lookupScore(anyInt())).thenReturn((byte) AccessPoint.SPEED_SLOW); + when(mockBadgeCurve.lookupScore(anyInt())).thenReturn((byte) AccessPoint.Speed.SLOW); ap.update(mockWifiNetworkScoreCache, true /* scoringUiEnabled */); - assertThat(ap.getSpeed()).isEqualTo(AccessPoint.SPEED_SLOW); + assertThat(ap.getSpeed()).isEqualTo(AccessPoint.Speed.SLOW); assertThat(ap.getSpeedLabel()) .isEqualTo(mContext.getString(R.string.speed_label_slow)); } @@ -394,7 +409,7 @@ public class AccessPointTest { when(mockWifiNetworkScoreCache.getScoredNetwork(any(ScanResult.class))) .thenReturn(buildScoredNetworkWithMockBadgeCurve()); - when(mockBadgeCurve.lookupScore(anyInt())).thenReturn((byte) AccessPoint.SPEED_VERY_FAST); + when(mockBadgeCurve.lookupScore(anyInt())).thenReturn((byte) AccessPoint.Speed.VERY_FAST); ap.update(mockWifiNetworkScoreCache, true /* scoringUiEnabled */); @@ -408,7 +423,7 @@ public class AccessPointTest { when(mockWifiNetworkScoreCache.getScoredNetwork(any(ScanResult.class))) .thenReturn(buildScoredNetworkWithMockBadgeCurve()); - when(mockBadgeCurve.lookupScore(anyInt())).thenReturn((byte) AccessPoint.SPEED_VERY_FAST); + when(mockBadgeCurve.lookupScore(anyInt())).thenReturn((byte) AccessPoint.Speed.VERY_FAST); ap.update(mockWifiNetworkScoreCache, true /* scoringUiEnabled */); diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/wifi/WifiTrackerTest.java b/packages/SettingsLib/tests/integ/src/com/android/settingslib/wifi/WifiTrackerTest.java index 340ef016d74d..073da7eea25f 100644 --- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/wifi/WifiTrackerTest.java +++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/wifi/WifiTrackerTest.java @@ -94,7 +94,7 @@ public class WifiTrackerTest { new NetworkKey(new WifiKey('"' + SSID_1 + '"', BSSID_1)); private static final int RSSI_1 = -30; private static final byte SCORE_1 = 10; - private static final int BADGE_1 = AccessPoint.SPEED_MEDIUM; + private static final int BADGE_1 = AccessPoint.Speed.MODERATE; private static final String SSID_2 = "ssid2"; private static final String BSSID_2 = "AA:AA:AA:AA:AA:AA"; @@ -102,7 +102,7 @@ public class WifiTrackerTest { new NetworkKey(new WifiKey('"' + SSID_2 + '"', BSSID_2)); private static final int RSSI_2 = -30; private static final byte SCORE_2 = 15; - private static final int BADGE_2 = AccessPoint.SPEED_FAST; + private static final int BADGE_2 = AccessPoint.Speed.FAST; private static final int CONNECTED_NETWORK_ID = 123; private static final int CONNECTED_RSSI = -50; @@ -256,6 +256,7 @@ public class WifiTrackerTest { } sendScanResultsAndProcess(tracker); + waitForHandlersToProcessCurrentlyEnqueuedMessages(tracker); return tracker; } @@ -341,6 +342,23 @@ public class WifiTrackerTest { return createTrackerWithImmediateBroadcastsAndInjectInitialScanResults(intent); } + private void waitForHandlersToProcessCurrentlyEnqueuedMessages(WifiTracker tracker) + throws InterruptedException { + CountDownLatch workerLatch = new CountDownLatch(1); + tracker.mWorkHandler.post(() -> { + workerLatch.countDown(); + }); + assertTrue("Latch timed out while waiting for WorkerHandler", + workerLatch.await(LATCH_TIMEOUT, TimeUnit.MILLISECONDS)); + + CountDownLatch mainLatch = new CountDownLatch(1); + tracker.mMainHandler.post(() -> { + mainLatch.countDown(); + }); + assertTrue("Latch timed out while waiting for MainHandler", + mainLatch.await(LATCH_TIMEOUT, TimeUnit.MILLISECONDS)); + } + @Test public void testAccessPointListenerSetWhenLookingUpUsingScanResults() { ScanResult scanResult = new ScanResult(); @@ -426,7 +444,7 @@ public class WifiTrackerTest { @Test public void startTrackingShouldSetConnectedAccessPointAsActive() throws InterruptedException { - WifiTracker tracker = createTrackerWithScanResultsAndAccessPoint1Connected(); + WifiTracker tracker = createTrackerWithScanResultsAndAccessPoint1Connected(); List<AccessPoint> aps = tracker.getAccessPoints(); @@ -460,16 +478,18 @@ public class WifiTrackerTest { startTracking(tracker); sendScanResultsAndProcess(tracker); - updateScoresAndWaitForAccessPointsChangedCallback(); + updateScoresAndWaitForAccessPointsChangedCallback(tracker); } - private void updateScoresAndWaitForAccessPointsChangedCallback() throws InterruptedException { + private void updateScoresAndWaitForAccessPointsChangedCallback(WifiTracker tracker) + throws InterruptedException { // Updating scores can happen together or one after the other, so the latch countdown is set // to 2. - mAccessPointsChangedLatch = new CountDownLatch(2); + mAccessPointsChangedLatch = new CountDownLatch(1); updateScores(); - assertTrue("onAccessPointChanged was not called twice", + assertTrue("onAccessPointChanged was not called after updating scores", mAccessPointsChangedLatch.await(LATCH_TIMEOUT, TimeUnit.MILLISECONDS)); + waitForHandlersToProcessCurrentlyEnqueuedMessages(tracker); } @Test @@ -480,7 +500,7 @@ public class WifiTrackerTest { assertEquals(aps.get(0).getSsidStr(), SSID_1); assertEquals(aps.get(1).getSsidStr(), SSID_2); - updateScoresAndWaitForAccessPointsChangedCallback(); + updateScoresAndWaitForAccessPointsChangedCallback(tracker); aps = tracker.getAccessPoints(); assertTrue(aps.size() == 2); @@ -502,7 +522,7 @@ public class WifiTrackerTest { assertEquals(aps.get(0).getSsidStr(), SSID_1); assertEquals(aps.get(1).getSsidStr(), SSID_2); - updateScoresAndWaitForAccessPointsChangedCallback(); + updateScoresAndWaitForAccessPointsChangedCallback(tracker); aps = tracker.getAccessPoints(); assertTrue(aps.size() == 2); @@ -514,7 +534,7 @@ public class WifiTrackerTest { public void scoreCacheUpdateScoresShouldInsertSpeedIntoAccessPoint() throws InterruptedException { WifiTracker tracker = createTrackerWithImmediateBroadcastsAndInjectInitialScanResults(); - updateScoresAndWaitForAccessPointsChangedCallback(); + updateScoresAndWaitForAccessPointsChangedCallback(tracker); List<AccessPoint> aps = tracker.getAccessPoints(); @@ -531,7 +551,7 @@ public class WifiTrackerTest { public void scoreCacheUpdateMeteredShouldUpdateAccessPointMetering() throws InterruptedException { WifiTracker tracker = createTrackerWithImmediateBroadcastsAndInjectInitialScanResults(); - updateScoresAndWaitForAccessPointsChangedCallback(); + updateScoresAndWaitForAccessPointsChangedCallback(tracker); List<AccessPoint> aps = tracker.getAccessPoints(); @@ -553,15 +573,15 @@ public class WifiTrackerTest { 0 /* disabled */); WifiTracker tracker = createTrackerWithImmediateBroadcastsAndInjectInitialScanResults(); - updateScoresAndWaitForAccessPointsChangedCallback(); + updateScoresAndWaitForAccessPointsChangedCallback(tracker); List<AccessPoint> aps = tracker.getAccessPoints(); for (AccessPoint ap : aps) { if (ap.getSsidStr().equals(SSID_1)) { - assertEquals(AccessPoint.SPEED_NONE, ap.getSpeed()); + assertEquals(AccessPoint.Speed.NONE, ap.getSpeed()); } else if (ap.getSsidStr().equals(SSID_2)) { - assertEquals(AccessPoint.SPEED_NONE, ap.getSpeed()); + assertEquals(AccessPoint.Speed.NONE, ap.getSpeed()); } } } @@ -754,13 +774,10 @@ public class WifiTrackerTest { throws Exception { WifiTracker tracker = createMockedWifiTracker(); startTracking(tracker); - tracker.stopTracking(); + waitForHandlersToProcessCurrentlyEnqueuedMessages(tracker); - CountDownLatch latch1 = new CountDownLatch(1); - tracker.mMainHandler.post(() -> { - latch1.countDown(); - }); - assertTrue("Latch 1 timed out", latch1.await(LATCH_TIMEOUT, TimeUnit.MILLISECONDS)); + tracker.stopTracking(); + waitForHandlersToProcessCurrentlyEnqueuedMessages(tracker); startTracking(tracker); @@ -770,14 +787,24 @@ public class WifiTrackerTest { tracker.mReceiver.onReceive( mContext, new Intent(WifiManager.LINK_CONFIGURATION_CHANGED_ACTION)); - CountDownLatch latch2 = new CountDownLatch(1); - tracker.mMainHandler.post(() -> { - latch2.countDown(); - }); - assertTrue("Latch 2 timed out", latch2.await(LATCH_TIMEOUT, TimeUnit.MILLISECONDS)); + waitForHandlersToProcessCurrentlyEnqueuedMessages(tracker); verify(mockWifiListener, never()).onAccessPointsChanged(); sendScanResultsAndProcess(tracker); // verifies onAccessPointsChanged is invoked } + + @Test + public void disablingWifiShouldClearExistingAccessPoints() throws Exception { + WifiTracker tracker = createTrackerWithScanResultsAndAccessPoint1Connected(); + + when(mockWifiManager.isWifiEnabled()).thenReturn(false); + mAccessPointsChangedLatch = new CountDownLatch(1); + tracker.mReceiver.onReceive(mContext, new Intent(WifiManager.WIFI_STATE_CHANGED_ACTION)); + + mAccessPointsChangedLatch.await(LATCH_TIMEOUT, TimeUnit.MILLISECONDS); + waitForHandlersToProcessCurrentlyEnqueuedMessages(tracker); + + assertThat(tracker.getAccessPoints()).isEmpty(); + } } diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginActivity.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginActivity.java new file mode 100644 index 000000000000..925214e3ab3a --- /dev/null +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginActivity.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.systemui.plugins; + +import android.app.Activity; +import android.content.Context; +import android.content.res.Resources; +import android.os.Bundle; + +import com.android.systemui.plugins.annotations.ProvidesInterface; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * A PluginActivity is an activity that replaces another full activity (e.g. RecentsActivity) + * at runtime within the sysui process. + */ +@ProvidesInterface(version = PluginActivity.VERSION) +public abstract class PluginActivity extends Activity implements Plugin { + + public static final int VERSION = 1; + + public static final String ACTION_RECENTS = "com.android.systemui.action.PLUGIN_RECENTS"; + + private Context mSysuiContext; + private boolean mSettingActionBar; + + @Override + public final void onCreate(Context sysuiContext, Context pluginContext) { + mSysuiContext = sysuiContext; + super.attachBaseContext(pluginContext); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Theme theme = getClass().getDeclaredAnnotation(Theme.class); + if (theme != null && theme.value() != 0) { + setTheme(theme.value()); + } + mSettingActionBar = true; + getActionBar(); + mSettingActionBar = false; + } + + @Override + public Resources getResources() { + return mSettingActionBar ? mSysuiContext.getResources() : super.getResources(); + } + + @Override + protected void attachBaseContext(Context newBase) { + mSysuiContext = newBase; + } + + @Override + public void onDestroy() { + super.onDestroy(); + } + + public Context getSysuiContext() { + return mSysuiContext; + } + + public Context getPluginContext() { + return getBaseContext(); + } + + /** + * Since PluginActivities are declared as services instead of activities (since they + * are plugins), they can't have a theme attached to them. Instead a PluginActivity + * can annotate itself with @Theme to specify the resource of the style it wants + * to be themed with. + */ + @Retention(RetentionPolicy.RUNTIME) + public @interface Theme { + int value(); + } +} diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginDependency.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginDependency.java index 25ce3ddf8169..db2e3765d2d0 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginDependency.java +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/PluginDependency.java @@ -21,6 +21,11 @@ public class PluginDependency { public static final int VERSION = 1; static DependencyProvider sProvider; + /** + * Allows a plugin to get a hold of static dependencies if they have declared dependence + * on their interface. For one-shot plugins this will only work during onCreate and will + * not work afterwards. + */ public static <T> T get(Plugin p, Class<T> cls) { return sProvider.get(p, cls); } diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QS.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QS.java index d57124381c1f..a648345e9c04 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QS.java +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QS.java @@ -31,9 +31,9 @@ import com.android.systemui.plugins.qs.QS.HeightListener; @DependsOn(target = HeightListener.class) public interface QS extends FragmentBase { - public static final String ACTION = "com.android.systemui.action.PLUGIN_QS"; + String ACTION = "com.android.systemui.action.PLUGIN_QS"; - public static final int VERSION = 6; + int VERSION = 6; String TAG = "QS"; @@ -66,8 +66,8 @@ public interface QS extends FragmentBase { } @ProvidesInterface(version = HeightListener.VERSION) - public interface HeightListener { - public static final int VERSION = 1; + interface HeightListener { + int VERSION = 1; void onQsHeightChanged(); } diff --git a/packages/SystemUI/res/drawable/car_qs_background_primary.xml b/packages/SystemUI/res/drawable/car_qs_background_primary.xml new file mode 100644 index 000000000000..0f77987bb7ce --- /dev/null +++ b/packages/SystemUI/res/drawable/car_qs_background_primary.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2017 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<inset xmlns:android="http://schemas.android.com/apk/res/android"> + <shape> + <solid android:color="?android:attr/colorPrimaryDark"/> + </shape> +</inset> diff --git a/packages/SystemUI/res/drawable/recents_dismiss_dark.xml b/packages/SystemUI/res/drawable/recents_dismiss_dark.xml index 951269bd37b1..b837ebef78eb 100644 --- a/packages/SystemUI/res/drawable/recents_dismiss_dark.xml +++ b/packages/SystemUI/res/drawable/recents_dismiss_dark.xml @@ -14,11 +14,16 @@ Copyright (C) 2014 The Android Open Source Project limitations under the License. --> <vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="48.0dp" - android:height="48.0dp" - android:viewportWidth="48.0" - android:viewportHeight="48.0"> - <path - android:fillColor="@color/recents_task_bar_dark_icon_color" - android:pathData="M38.000000,12.800000l-2.799999,-2.800000 -11.200001,11.200001 -11.200000,-11.200001 -2.800000,2.800000 11.200001,11.200000 -11.200001,11.200001 2.800000,2.799999 11.200000,-11.200001 11.200001,11.200001 2.799999,-2.799999 -11.200001,-11.200001z"/> +android:width="24dp" +android:height="24dp" +android:viewportWidth="24" +android:viewportHeight="24"> + +<path + android:fillColor="@color/recents_task_bar_dark_icon_color" + android:pathData="M18.3 5.71a.996 .996 0 0 0-1.41 0L12 10.59 7.11 5.7A.996 .996 0 1 0 5.7 +7.11L10.59 12 5.7 16.89a.996 .996 0 1 0 1.41 1.41L12 13.41l4.89 4.89a.996 .996 0 +1 0 1.41-1.41L13.41 12l4.89-4.89c.38-.38 .38 -1.02 0-1.4z" /> +<path + android:pathData="M0 0h24v24H0z" /> </vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/recents_dismiss_light.xml b/packages/SystemUI/res/drawable/recents_dismiss_light.xml index 1f44c1c846bb..2b2081404b69 100644 --- a/packages/SystemUI/res/drawable/recents_dismiss_light.xml +++ b/packages/SystemUI/res/drawable/recents_dismiss_light.xml @@ -14,11 +14,16 @@ Copyright (C) 2014 The Android Open Source Project limitations under the License. --> <vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="48.0dp" - android:height="48.0dp" - android:viewportWidth="48.0" - android:viewportHeight="48.0"> - <path - android:fillColor="@color/recents_task_bar_light_icon_color" - android:pathData="M38.000000,12.800000l-2.799999,-2.800000 -11.200001,11.200001 -11.200000,-11.200001 -2.800000,2.800000 11.200001,11.200000 -11.200001,11.200001 2.800000,2.799999 11.200000,-11.200001 11.200001,11.200001 2.799999,-2.799999 -11.200001,-11.200001z"/> +android:width="24dp" +android:height="24dp" +android:viewportWidth="24" +android:viewportHeight="24"> + +<path + android:fillColor="@color/recents_task_bar_light_icon_color" + android:pathData="M18.3 5.71a.996 .996 0 0 0-1.41 0L12 10.59 7.11 5.7A.996 .996 0 1 0 5.7 +7.11L10.59 12 5.7 16.89a.996 .996 0 1 0 1.41 1.41L12 13.41l4.89 4.89a.996 .996 0 +1 0 1.41-1.41L13.41 12l4.89-4.89c.38-.38 .38 -1.02 0-1.4z" /> +<path + android:pathData="M0 0h24v24H0z" /> </vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/back.xml b/packages/SystemUI/res/layout/back.xml index 4e8726b3a634..43bec91ad053 100644 --- a/packages/SystemUI/res/layout/back.xml +++ b/packages/SystemUI/res/layout/back.xml @@ -22,8 +22,10 @@ android:layout_height="match_parent" android:layout_weight="0" systemui:keyCode="4" - android:scaleType="center" + android:scaleType="fitCenter" android:contentDescription="@string/accessibility_back" + android:paddingTop="15dp" + android:paddingBottom="15dp" android:paddingStart="@dimen/navigation_key_padding" android:paddingEnd="@dimen/navigation_key_padding" /> diff --git a/packages/SystemUI/res/layout/car_qs_footer.xml b/packages/SystemUI/res/layout/car_qs_footer.xml new file mode 100644 index 000000000000..2ef3b05f0565 --- /dev/null +++ b/packages/SystemUI/res/layout/car_qs_footer.xml @@ -0,0 +1,63 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2017 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<!-- extends RelativeLayout --> +<com.android.systemui.qs.car.CarQSFooter + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/qs_footer" + android:layout_width="match_parent" + android:layout_height="@dimen/qs_footer_height" + android:baselineAligned="false" + android:clickable="false" + android:clipChildren="false" + android:clipToPadding="false" + android:paddingBottom="16dp" + android:paddingTop="16dp" + android:paddingEnd="32dp" + android:paddingStart="32dp" + android:gravity="center_vertical" + android:orientation="horizontal"> + + <com.android.systemui.statusbar.phone.MultiUserSwitch + android:id="@+id/multi_user_switch" + android:layout_alignParentStart="true" + android:layout_centerVertical="true" + android:layout_width="48dp" + android:layout_height="48dp" + android:background="@drawable/ripple_drawable" + android:focusable="true"> + + <ImageView + android:id="@+id/multi_user_avatar" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_gravity="center" + android:scaleType="centerInside"/> + </com.android.systemui.statusbar.phone.MultiUserSwitch> + + <com.android.systemui.statusbar.phone.SettingsButton + android:id="@+id/settings_button" + android:layout_alignParentEnd="true" + android:layout_centerVertical="true" + android:layout_width="48dp" + android:layout_height="48dp" + android:background="@drawable/ripple_drawable" + android:contentDescription="@string/accessibility_quick_settings_settings" + android:scaleType="centerCrop" + android:src="@drawable/ic_settings_16dp" + android:tint="?android:attr/colorForeground" + style="@android:style/Widget.Material.Button.Borderless" /> + +</com.android.systemui.qs.car.CarQSFooter> diff --git a/packages/SystemUI/res/layout/car_qs_panel.xml b/packages/SystemUI/res/layout/car_qs_panel.xml new file mode 100644 index 000000000000..d1f7ff83db93 --- /dev/null +++ b/packages/SystemUI/res/layout/car_qs_panel.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2017 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/quick_settings_container" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="@drawable/car_qs_background_primary" + android:orientation="vertical" + android:elevation="4dp"> + + <include layout="@layout/car_status_bar_header" /> + <include layout="@layout/car_qs_footer" /> +</LinearLayout> diff --git a/packages/SystemUI/res/layout/car_status_bar_header.xml b/packages/SystemUI/res/layout/car_status_bar_header.xml new file mode 100644 index 000000000000..158907e03541 --- /dev/null +++ b/packages/SystemUI/res/layout/car_status_bar_header.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2017 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<!-- Extends RelativeLayout --> +<com.android.systemui.qs.car.CarStatusBarHeader + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:systemui="http://schemas.android.com/apk/res-auto" + android:id="@+id/header" + android:layout_width="match_parent" + android:layout_height="@dimen/car_qs_header_system_icons_area_height" + android:paddingStart="8dp" + android:paddingEnd="8dp" > + + <include + layout="@layout/system_icons" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:layout_alignParentStart="true" + android:layout_centerVertical="true" /> + + <com.android.systemui.statusbar.policy.Clock + android:id="@+id/clock" + android:textAppearance="@style/TextAppearance.StatusBar.Clock" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:layout_alignParentEnd="true" + android:layout_centerVertical="true" + android:singleLine="true" + android:paddingStart="@dimen/status_bar_clock_starting_padding" + android:paddingEnd="@dimen/status_bar_clock_end_padding" + systemui:showDark="false" /> +</com.android.systemui.qs.car.CarStatusBarHeader> diff --git a/packages/SystemUI/res/layout/home.xml b/packages/SystemUI/res/layout/home.xml index 95863272b9bf..53ef2ab247e7 100644 --- a/packages/SystemUI/res/layout/home.xml +++ b/packages/SystemUI/res/layout/home.xml @@ -21,8 +21,10 @@ android:layout_height="match_parent" android:layout_weight="0" systemui:keyCode="3" - android:scaleType="center" + android:scaleType="fitCenter" android:contentDescription="@string/accessibility_home" + android:paddingTop="13dp" + android:paddingBottom="13dp" android:paddingStart="@dimen/navigation_key_padding" android:paddingEnd="@dimen/navigation_key_padding" /> diff --git a/packages/SystemUI/res/layout/qs_footer.xml b/packages/SystemUI/res/layout/qs_footer_impl.xml index db39905cf52a..43e88ba32736 100644 --- a/packages/SystemUI/res/layout/qs_footer.xml +++ b/packages/SystemUI/res/layout/qs_footer_impl.xml @@ -15,10 +15,10 @@ ** limitations under the License. --> -<!-- Extends RelativeLayout --> -<com.android.systemui.qs.QSFooter +<!-- Extends FrameLayout --> +<com.android.systemui.qs.QSFooterImpl xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@+id/header" + android:id="@+id/qs_footer" android:layout_width="match_parent" android:layout_height="48dp" android:baselineAligned="false" @@ -114,4 +114,4 @@ android:padding="14dp" /> </LinearLayout> -</com.android.systemui.qs.QSFooter> +</com.android.systemui.qs.QSFooterImpl> diff --git a/packages/SystemUI/res/layout/qs_panel.xml b/packages/SystemUI/res/layout/qs_panel.xml index 87101e4b3d14..5541f3de8e7c 100644 --- a/packages/SystemUI/res/layout/qs_panel.xml +++ b/packages/SystemUI/res/layout/qs_panel.xml @@ -32,8 +32,7 @@ <include layout="@layout/quick_status_bar_expanded_header" /> - <include android:id="@+id/qs_footer" - layout="@layout/qs_footer" /> + <include layout="@layout/qs_footer_impl" /> <include android:id="@+id/qs_detail" layout="@layout/qs_detail" /> diff --git a/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml b/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml index 2ff626aa89a9..e8b418cd902b 100644 --- a/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml +++ b/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml @@ -18,7 +18,6 @@ <!-- Extends RelativeLayout --> <com.android.systemui.qs.QuickStatusBarHeader xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:systemui="http://schemas.android.com/apk/res-auto" android:id="@+id/header" android:layout_width="match_parent" android:layout_height="@dimen/status_bar_header_height" @@ -31,46 +30,7 @@ android:paddingEnd="0dp" android:paddingStart="0dp"> - <LinearLayout - android:layout_width="match_parent" - android:layout_height="32dp" - android:layout_alignParentEnd="true" - android:clipChildren="false" - android:clipToPadding="false" - android:gravity="center" - android:paddingStart="16dp" - android:paddingEnd="16dp" - android:orientation="horizontal"> - - - <com.android.keyguard.CarrierText - android:id="@+id/qs_carrier_text" - android:layout_width="0dp" - android:layout_height="match_parent" - android:layout_weight="1" - android:gravity="center_vertical|start" - android:ellipsize="marquee" - android:textAppearance="?android:attr/textAppearanceSmall" - android:textColor="?android:attr/textColorPrimary" - android:singleLine="true" /> - - <com.android.systemui.BatteryMeterView android:id="@+id/battery" - android:layout_height="match_parent" - android:layout_width="wrap_content" - /> - - <com.android.systemui.statusbar.policy.Clock - android:id="@+id/clock" - android:textAppearance="@style/TextAppearance.StatusBar.Clock" - android:layout_width="wrap_content" - android:layout_height="match_parent" - android:singleLine="true" - android:paddingStart="@dimen/status_bar_clock_starting_padding" - android:paddingEnd="@dimen/status_bar_clock_end_padding" - android:gravity="center_vertical|start" - systemui:showDark="false" - /> - </LinearLayout> + <include layout="@layout/quick_status_bar_header_system_icons" /> <com.android.systemui.qs.QuickQSPanel android:id="@+id/quick_qs_panel" diff --git a/packages/SystemUI/res/layout/quick_status_bar_header_system_icons.xml b/packages/SystemUI/res/layout/quick_status_bar_header_system_icons.xml new file mode 100644 index 000000000000..c6dbd18a7d12 --- /dev/null +++ b/packages/SystemUI/res/layout/quick_status_bar_header_system_icons.xml @@ -0,0 +1,58 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +** Copyright 2017, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +--> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:systemui="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="@dimen/qs_header_system_icons_area_height" + android:layout_alignParentEnd="true" + android:clipChildren="false" + android:clipToPadding="false" + android:gravity="center" + android:paddingStart="16dp" + android:paddingEnd="16dp" + android:orientation="horizontal"> + + + <com.android.keyguard.CarrierText + android:id="@+id/qs_carrier_text" + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight="1" + android:gravity="center_vertical|start" + android:ellipsize="marquee" + android:textAppearance="?android:attr/textAppearanceSmall" + android:textColor="?android:attr/textColorPrimary" + android:singleLine="true" /> + + <com.android.systemui.BatteryMeterView android:id="@+id/battery" + android:layout_height="match_parent" + android:layout_width="wrap_content" + /> + + <com.android.systemui.statusbar.policy.Clock + android:id="@+id/clock" + android:textAppearance="@style/TextAppearance.StatusBar.Clock" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:singleLine="true" + android:paddingStart="@dimen/status_bar_clock_starting_padding" + android:paddingEnd="@dimen/status_bar_clock_end_padding" + android:gravity="center_vertical|start" + systemui:showDark="false" + /> +</LinearLayout> diff --git a/packages/SystemUI/res/layout/recent_apps.xml b/packages/SystemUI/res/layout/recent_apps.xml index 870bcf7547a7..c84d28051286 100644 --- a/packages/SystemUI/res/layout/recent_apps.xml +++ b/packages/SystemUI/res/layout/recent_apps.xml @@ -21,8 +21,10 @@ android:layout_width="@dimen/navigation_key_width" android:layout_height="match_parent" android:layout_weight="0" - android:scaleType="center" + android:scaleType="fitCenter" android:contentDescription="@string/accessibility_recent" + android:paddingTop="15dp" + android:paddingBottom="15dp" android:paddingStart="@dimen/navigation_key_padding" android:paddingEnd="@dimen/navigation_key_padding" /> diff --git a/packages/SystemUI/res/values-car/dimens.xml b/packages/SystemUI/res/values-car/dimens.xml new file mode 100644 index 000000000000..b2e7bd1b881e --- /dev/null +++ b/packages/SystemUI/res/values-car/dimens.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + * Copyright (c) 2017, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ +--> +<resources> + <!-- The height of the quick settings footer that holds the user switcher, settings icon, + etc. in the car setting.--> + <dimen name="qs_footer_height">74dp</dimen> +</resources> diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 2ad6f2da7fb0..c0068d3b2957 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -316,26 +316,6 @@ <!-- Whether or not a background should be drawn behind a notification. --> <bool name="config_drawNotificationBackground">true</bool> - <!-- Whether or not the edit icon on the quick settings header is shown. --> - <bool name="config_showQuickSettingsEditingIcon">true</bool> - - <!-- Whether or not the multi-user switcher should be visible even if the quick settings are - not expanded. If there are not multiple users on the system, the switcher will still - hide itself. --> - <bool name="config_alwaysShowMultiUserSwitcher">false</bool> - - <!-- Whether or not the expand indicator is visible for manually expanding the quick settings - panel. --> - <bool name="config_showQuickSettingsExpandIndicator">true</bool> - - <!-- Whether or not to display the row of quick settings icons separate from the full quick - settings panel. --> - <bool name="config_showQuickSettingsRow">true</bool> - - <!-- Whether or not the quick settings should be revealed on an overscroll of the - notifications panel. --> - <bool name="config_enableQuickSettingsOverscrollExpansion">true</bool> - <!-- Whether or the notifications can be shown and dismissed with a drag. --> <bool name="config_enableNotificationShadeDrag">true</bool> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 93d2072e955c..c37069f4c6c1 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -189,6 +189,23 @@ <!-- Height of the status bar header bar --> <dimen name="status_bar_header_height">124dp</dimen> + <!-- Height of the status bar header bar in the car setting. --> + <dimen name="car_status_bar_header_height">128dp</dimen> + + <!-- The bottom padding of the status bar header. --> + <dimen name="status_bar_header_padding_bottom">48dp</dimen> + + <!-- The height of the container that holds the system icons in the quick settings header. --> + <dimen name="qs_header_system_icons_area_height">32dp</dimen> + + <!-- The height of the container that holds the system icons in the quick settings header in the + car setting. --> + <dimen name="car_qs_header_system_icons_area_height">54dp</dimen> + + <!-- The height of the quick settings footer that holds the user switcher, settings icon, + etc. --> + <dimen name="qs_footer_height">48dp</dimen> + <!-- Height of the status bar header bar when expanded --> <dimen name="status_bar_header_height_expanded">124dp</dimen> @@ -827,7 +844,4 @@ <!-- How far to inset the rounded edges --> <dimen name="stat_sys_mobile_signal_circle_inset">0.9dp</dimen> - <!-- Width of the hollow triangle for empty signal state --> - <dimen name="mobile_signal_empty_strokewidth">2dp</dimen> - </resources> diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java index 1b694b372124..bb4412375ff8 100644 --- a/packages/SystemUI/src/com/android/systemui/Dependency.java +++ b/packages/SystemUI/src/com/android/systemui/Dependency.java @@ -31,6 +31,7 @@ import com.android.systemui.assist.AssistManager; import com.android.systemui.colorextraction.SysuiColorExtractor; import com.android.systemui.fragments.FragmentService; import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.plugins.PluginActivityManager; import com.android.systemui.plugins.PluginDependencyProvider; import com.android.systemui.plugins.PluginManager; import com.android.systemui.plugins.PluginManagerImpl; @@ -276,6 +277,9 @@ public class Dependency extends SystemUI { mProviders.put(UiOffloadThread.class, UiOffloadThread::new); + mProviders.put(PluginActivityManager.class, + () -> new PluginActivityManager(mContext, getDependency(PluginManager.class))); + // Put all dependencies above here so the factory can override them if it wants. SystemUIFactory.getInstance().injectDependencies(mProviders, mContext); } diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java index 9034c3fd926e..211f0c75b5dd 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java +++ b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java @@ -16,6 +16,7 @@ package com.android.systemui; +import android.app.Activity; import android.app.ActivityThread; import android.app.Application; import android.content.BroadcastReceiver; @@ -41,6 +42,7 @@ import com.android.systemui.pip.PipUI; import com.android.systemui.plugins.GlobalActions; import com.android.systemui.plugins.OverlayPlugin; import com.android.systemui.plugins.Plugin; +import com.android.systemui.plugins.PluginActivityManager; import com.android.systemui.plugins.PluginListener; import com.android.systemui.plugins.PluginManager; import com.android.systemui.power.PowerUI; @@ -281,4 +283,10 @@ public class SystemUIApplication extends Application implements SysUiServiceProv public SystemUI[] getServices() { return mServices; } + + @Override + public Activity instantiateActivity(ClassLoader cl, String className, Intent intent) { + if (!mServicesStarted) return null; + return Dependency.get(PluginActivityManager.class).instantiate(cl, className, intent); + } } diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java index 1ece5fc0063f..8d1d6e0ce460 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java @@ -39,6 +39,7 @@ import com.android.systemui.util.Assert; import com.android.systemui.util.wakelock.WakeLock; import java.io.PrintWriter; +import java.util.function.IntConsumer; /** * Handles triggers for ambient state changes. @@ -98,18 +99,44 @@ public class DozeTriggers implements DozeMachine.Part { requestPulse(DozeLog.PULSE_REASON_NOTIFICATION, false /* performedProxCheck */); } + private void proximityCheckThenCall(IntConsumer callback, + boolean alreadyPerformedProxCheck, + int pulseReason) { + if (alreadyPerformedProxCheck) { + callback.accept(ProximityCheck.RESULT_NOT_CHECKED); + } else { + final long start = SystemClock.uptimeMillis(); + new ProximityCheck() { + @Override + public void onProximityResult(int result) { + final long end = SystemClock.uptimeMillis(); + DozeLog.traceProximityResult(mContext, result == RESULT_NEAR, + end - start, pulseReason); + callback.accept(result); + } + }.check(); + } + } + private void onSensor(int pulseReason, boolean sensorPerformedProxCheck, float screenX, float screenY) { boolean isDoubleTap = pulseReason == DozeLog.PULSE_REASON_SENSOR_DOUBLE_TAP; boolean isPickup = pulseReason == DozeLog.PULSE_REASON_SENSOR_PICKUP; if (mConfig.alwaysOnEnabled(UserHandle.USER_CURRENT)) { - if (isDoubleTap) { - mDozeHost.onDoubleTap(screenX, screenY); - mMachine.wakeUp(); - } else { - mDozeHost.extendPulse(); - } + proximityCheckThenCall((result) -> { + if (result == ProximityCheck.RESULT_NEAR) { + // In pocket, drop event. + return; + } + if (isDoubleTap) { + mDozeHost.onDoubleTap(screenX, screenY); + mMachine.wakeUp(); + } else { + mDozeHost.extendPulse(); + } + }, sensorPerformedProxCheck, pulseReason); + return; } else { requestPulse(pulseReason, sensorPerformedProxCheck); } @@ -202,33 +229,15 @@ public class DozeTriggers implements DozeMachine.Part { } mPulsePending = true; - if (!mDozeParameters.getProxCheckBeforePulse() || performedProxCheck) { - // skip proximity check - continuePulseRequest(reason); - return; - } - - final long start = SystemClock.uptimeMillis(); - new ProximityCheck() { - @Override - public void onProximityResult(int result) { - final long end = SystemClock.uptimeMillis(); - DozeLog.traceProximityResult(mContext, result == RESULT_NEAR, - end - start, reason); - if (performedProxCheck) { - // we already continued - return; - } - // avoid pulsing in pockets - if (result == RESULT_NEAR) { - mPulsePending = false; - return; - } - - // not in-pocket, continue pulsing + proximityCheckThenCall((result) -> { + if (result == ProximityCheck.RESULT_NEAR) { + // in pocket, abort pulse + mPulsePending = false; + } else { + // not in pocket, continue pulsing continuePulseRequest(reason); } - }.check(); + }, !mDozeParameters.getProxCheckBeforePulse() || performedProxCheck, reason); } private boolean canPulse() { @@ -262,6 +271,7 @@ public class DozeTriggers implements DozeMachine.Part { protected static final int RESULT_UNKNOWN = 0; protected static final int RESULT_NEAR = 1; protected static final int RESULT_FAR = 2; + protected static final int RESULT_NOT_CHECKED = 3; private boolean mRegistered; private boolean mFinished; diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java index 80a641870f5b..1a8a474a4187 100644 --- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java +++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java @@ -339,6 +339,7 @@ class GlobalActionsDialog implements DialogInterface.OnDismissListener, DialogIn long id) { final Action action = mAdapter.getItem(position); if (action instanceof LongPressAction) { + mDialog.dismiss(); return ((LongPressAction) action).onLongPress(); } return false; diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipMotionHelper.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipMotionHelper.java index b8771d7e0fb6..cebb22f07aaf 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipMotionHelper.java +++ b/packages/SystemUI/src/com/android/systemui/pip/phone/PipMotionHelper.java @@ -241,14 +241,14 @@ public class PipMotionHelper implements Handler.Callback { /** * Flings the minimized PiP to the closest minimized snap target. */ - Rect flingToMinimizedState(float velocityY, Rect movementBounds) { + Rect flingToMinimizedState(float velocityY, Rect movementBounds, Point dragStartPosition) { cancelAnimations(); // We currently only allow flinging the minimized stack up and down, so just lock the // movement bounds to the current stack bounds horizontally movementBounds = new Rect(mBounds.left, movementBounds.top, mBounds.left, movementBounds.bottom); Rect toBounds = mSnapAlgorithm.findClosestSnapBounds(movementBounds, mBounds, - 0 /* velocityX */, velocityY); + 0 /* velocityX */, velocityY, dragStartPosition); if (!mBounds.equals(toBounds)) { mBoundsAnimator = createAnimationToBounds(mBounds, toBounds, 0, FAST_OUT_SLOW_IN); mFlingAnimationUtils.apply(mBoundsAnimator, 0, @@ -281,10 +281,11 @@ public class PipMotionHelper implements Handler.Callback { * Flings the PiP to the closest snap target. */ Rect flingToSnapTarget(float velocity, float velocityX, float velocityY, Rect movementBounds, - AnimatorUpdateListener updateListener, AnimatorListener listener) { + AnimatorUpdateListener updateListener, AnimatorListener listener, + Point startPosition) { cancelAnimations(); Rect toBounds = mSnapAlgorithm.findClosestSnapBounds(movementBounds, mBounds, - velocityX, velocityY); + velocityX, velocityY, startPosition); if (!mBounds.equals(toBounds)) { mBoundsAnimator = createAnimationToBounds(mBounds, toBounds, 0, FAST_OUT_SLOW_IN); mFlingAnimationUtils.apply(mBoundsAnimator, 0, diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java index 3682ae655f7c..9588b03b53bd 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java +++ b/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java @@ -185,7 +185,7 @@ public class PipTouchHandler { mDismissViewController = new PipDismissViewController(context); mSnapAlgorithm = new PipSnapAlgorithm(mContext); mTouchState = new PipTouchState(mViewConfig); - mFlingAnimationUtils = new FlingAnimationUtils(context, 2f); + mFlingAnimationUtils = new FlingAnimationUtils(context, 2.5f); mGestures = new PipTouchGesture[] { mDefaultMovementGesture }; @@ -534,6 +534,7 @@ public class PipTouchHandler { private PipTouchGesture mDefaultMovementGesture = new PipTouchGesture() { // Whether the PiP was on the left side of the screen at the start of the gesture private boolean mStartedOnLeft; + private Point mStartPosition; @Override public void onDown(PipTouchState touchState) { @@ -541,7 +542,9 @@ public class PipTouchHandler { return; } - mStartedOnLeft = mMotionHelper.getBounds().left < mMovementBounds.centerX(); + Rect bounds = mMotionHelper.getBounds(); + mStartPosition = new Point(bounds.left, bounds.top); + mStartedOnLeft = bounds.left < mMovementBounds.centerX(); mMovementWithinMinimize = true; mMovementWithinDismiss = touchState.getDownTouchPosition().y >= mMovementBounds.bottom; @@ -687,7 +690,8 @@ public class PipTouchHandler { if (isFling) { mMotionHelper.flingToSnapTarget(velocity, vel.x, vel.y, mMovementBounds, - mUpdateScrimListener, postAnimationCallback); + mUpdateScrimListener, postAnimationCallback, + mStartPosition); } else { mMotionHelper.animateToClosestSnapTarget(mMovementBounds, mUpdateScrimListener, postAnimationCallback); diff --git a/packages/SystemUI/src/com/android/systemui/plugins/PluginActivityManager.java b/packages/SystemUI/src/com/android/systemui/plugins/PluginActivityManager.java new file mode 100644 index 000000000000..9becc38d760e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/plugins/PluginActivityManager.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.systemui.plugins; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.text.TextUtils; +import android.util.ArrayMap; + +public class PluginActivityManager { + + private final Context mContext; + private final PluginManager mPluginManager; + private final ArrayMap<String, String> mActionLookup = new ArrayMap<>(); + + public PluginActivityManager(Context context, PluginManager pluginManager) { + mContext = context; + mPluginManager = pluginManager; + } + + public void addActivityPlugin(String className, String action) { + mActionLookup.put(className, action); + } + + public Activity instantiate(ClassLoader cl, String className, Intent intent) { + String action = mActionLookup.get(className); + if (TextUtils.isEmpty(action)) return null; + return mPluginManager.getOneShotPlugin(action, PluginActivity.class); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/plugins/PluginManagerImpl.java b/packages/SystemUI/src/com/android/systemui/plugins/PluginManagerImpl.java index 493d244f5e99..a96839943cad 100644 --- a/packages/SystemUI/src/com/android/systemui/plugins/PluginManagerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/plugins/PluginManagerImpl.java @@ -42,7 +42,6 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.messages.nano.SystemMessageProto.SystemMessage; import com.android.systemui.Dependency; import com.android.systemui.plugins.PluginInstanceManager.PluginContextWrapper; -import com.android.systemui.plugins.PluginInstanceManager.PluginInfo; import com.android.systemui.plugins.annotations.ProvidesInterface; import dalvik.system.PathClassLoader; @@ -120,14 +119,21 @@ public class PluginManagerImpl extends BroadcastReceiver implements PluginManage } PluginInstanceManager<T> p = mFactory.createPluginInstanceManager(mContext, action, null, false, mLooper, cls, this); + PluginListener<Plugin> listener = new PluginListener<Plugin>() { + @Override + public void onPluginConnected(Plugin plugin, Context pluginContext) { } + }; + mPluginMap.put(listener, p); mPluginPrefs.addAction(action); - PluginInfo<T> info = p.getPlugin(); + PluginInstanceManager.PluginInfo<T> info = p.getPlugin(); if (info != null) { mOneShotPackages.add(info.mPackage); mHasOneShot = true; startListening(); + mPluginMap.remove(listener); return info.mPlugin; } + mPluginMap.remove(listener); return null; } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java index ed57c043a109..33b5268e03e1 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java @@ -38,7 +38,7 @@ public class QSContainerImpl extends FrameLayout { protected View mHeader; protected float mQsExpansion; private QSCustomizer mQSCustomizer; - private QSFooter mQSFooter; + private View mQSFooter; private float mFullElevation; public QSContainerImpl(Context context, AttributeSet attrs) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFooter.java b/packages/SystemUI/src/com/android/systemui/qs/QSFooter.java index 488fc03032fd..3f3cea2eaa17 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSFooter.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSFooter.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015 The Android Open Source Project + * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,426 +13,60 @@ * See the License for the specific language governing permissions and * limitations under the License */ - package com.android.systemui.qs; -import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.ACTION_QS_DATE; - -import android.app.ActivityManager; -import android.app.AlarmManager; -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.content.res.Configuration; -import android.content.res.Resources; -import android.graphics.PorterDuff.Mode; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.RippleDrawable; -import android.os.UserManager; -import android.provider.AlarmClock; import android.support.annotation.Nullable; -import android.support.annotation.VisibleForTesting; -import android.util.AttributeSet; import android.view.View; -import android.view.View.OnClickListener; -import android.widget.FrameLayout; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.TextView; -import android.widget.Toast; - -import com.android.internal.logging.MetricsLogger; -import com.android.internal.logging.nano.MetricsProto; -import com.android.keyguard.KeyguardStatusView; -import com.android.settingslib.Utils; -import com.android.systemui.Dependency; -import com.android.systemui.FontSizeUtils; -import com.android.systemui.R; -import com.android.systemui.R.dimen; -import com.android.systemui.R.id; -import com.android.systemui.plugins.ActivityStarter; -import com.android.systemui.qs.TouchAnimator.Builder; -import com.android.systemui.qs.TouchAnimator.Listener; -import com.android.systemui.qs.TouchAnimator.ListenerAdapter; -import com.android.systemui.statusbar.phone.ExpandableIndicator; -import com.android.systemui.statusbar.phone.MultiUserSwitch; -import com.android.systemui.statusbar.phone.SettingsButton; -import com.android.systemui.statusbar.policy.DeviceProvisionedController; -import com.android.systemui.statusbar.policy.NetworkController; -import com.android.systemui.statusbar.policy.NetworkController.EmergencyListener; -import com.android.systemui.statusbar.policy.NetworkController.SignalCallback; -import com.android.systemui.statusbar.policy.NextAlarmController; -import com.android.systemui.statusbar.policy.NextAlarmController.NextAlarmChangeCallback; -import com.android.systemui.statusbar.policy.UserInfoController; -import com.android.systemui.statusbar.policy.UserInfoController.OnUserInfoChangedListener; -import com.android.systemui.tuner.TunerService; - -public class QSFooter extends FrameLayout implements - NextAlarmChangeCallback, OnClickListener, OnUserInfoChangedListener, EmergencyListener, - SignalCallback { - private static final float EXPAND_INDICATOR_THRESHOLD = .93f; - - private ActivityStarter mActivityStarter; - private NextAlarmController mNextAlarmController; - private UserInfoController mUserInfoController; - private SettingsButton mSettingsButton; - protected View mSettingsContainer; - - private TextView mAlarmStatus; - private View mAlarmStatusCollapsed; - private View mDate; - - private QSPanel mQsPanel; - - private boolean mExpanded; - private boolean mAlarmShowing; - - protected ExpandableIndicator mExpandIndicator; - - private boolean mListening; - private AlarmManager.AlarmClockInfo mNextAlarm; - - private boolean mShowEmergencyCallsOnly; - protected MultiUserSwitch mMultiUserSwitch; - private ImageView mMultiUserAvatar; - private boolean mAlwaysShowMultiUserSwitch; - - protected TouchAnimator mSettingsAlpha; - private float mExpansionAmount; - - protected View mEdit; - private boolean mShowEditIcon; - private TouchAnimator mAnimator; - private View mDateTimeGroup; - private boolean mKeyguardShowing; - private TouchAnimator mAlarmAnimator; - - public QSFooter(Context context, AttributeSet attrs) { - super(context, attrs); - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - Resources res = getResources(); - - mShowEditIcon = res.getBoolean(R.bool.config_showQuickSettingsEditingIcon); - - mEdit = findViewById(android.R.id.edit); - mEdit.setVisibility(mShowEditIcon ? VISIBLE : GONE); - - if (mShowEditIcon) { - findViewById(android.R.id.edit).setOnClickListener(view -> - Dependency.get(ActivityStarter.class).postQSRunnableDismissingKeyguard(() -> - mQsPanel.showEdit(view))); - } - - mDateTimeGroup = findViewById(id.date_time_alarm_group); - mDate = findViewById(R.id.date); - - mExpandIndicator = findViewById(R.id.expand_indicator); - mExpandIndicator.setVisibility( - res.getBoolean(R.bool.config_showQuickSettingsExpandIndicator) - ? VISIBLE : GONE); - - mSettingsButton = findViewById(R.id.settings_button); - mSettingsContainer = findViewById(R.id.settings_button_container); - mSettingsButton.setOnClickListener(this); - - mAlarmStatusCollapsed = findViewById(R.id.alarm_status_collapsed); - mAlarmStatus = findViewById(R.id.alarm_status); - mDateTimeGroup.setOnClickListener(this); - - mMultiUserSwitch = findViewById(R.id.multi_user_switch); - mMultiUserAvatar = mMultiUserSwitch.findViewById(R.id.multi_user_avatar); - mAlwaysShowMultiUserSwitch = res.getBoolean(R.bool.config_alwaysShowMultiUserSwitcher); - - // RenderThread is doing more harm than good when touching the header (to expand quick - // settings), so disable it for this view - ((RippleDrawable) mSettingsButton.getBackground()).setForceSoftware(true); - ((RippleDrawable) mExpandIndicator.getBackground()).setForceSoftware(true); - - updateResources(); - - mNextAlarmController = Dependency.get(NextAlarmController.class); - mUserInfoController = Dependency.get(UserInfoController.class); - mActivityStarter = Dependency.get(ActivityStarter.class); - addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop, oldRight, - oldBottom) -> updateAnimator(right - left)); - } - - private void updateAnimator(int width) { - int numTiles = QuickQSPanel.getNumQuickTiles(mContext); - int size = mContext.getResources().getDimensionPixelSize(R.dimen.qs_quick_tile_size) - - mContext.getResources().getDimensionPixelSize(dimen.qs_quick_tile_padding); - int remaining = (width - numTiles * size) / (numTiles - 1); - int defSpace = mContext.getResources().getDimensionPixelOffset(R.dimen.default_gear_space); - - mAnimator = new Builder() - .addFloat(mSettingsContainer, "translationX", -(remaining - defSpace), 0) - .addFloat(mSettingsButton, "rotation", -120, 0) - .build(); - if (mAlarmShowing) { - mAlarmAnimator = new Builder().addFloat(mDate, "alpha", 1, 0) - .addFloat(mDateTimeGroup, "translationX", 0, -mDate.getWidth()) - .addFloat(mAlarmStatus, "alpha", 0, 1) - .setListener(new ListenerAdapter() { - @Override - public void onAnimationAtStart() { - mAlarmStatus.setVisibility(View.GONE); - } - - @Override - public void onAnimationStarted() { - mAlarmStatus.setVisibility(View.VISIBLE); - } - }).build(); - } else { - mAlarmAnimator = null; - mAlarmStatus.setVisibility(View.GONE); - mDate.setAlpha(1); - mDateTimeGroup.setTranslationX(0); - } - setExpansion(mExpansionAmount); - } - - @Override - protected void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); - updateResources(); - } - - @Override - public void onRtlPropertiesChanged(int layoutDirection) { - super.onRtlPropertiesChanged(layoutDirection); - updateResources(); - } - - private void updateResources() { - FontSizeUtils.updateFontSize(mAlarmStatus, R.dimen.qs_date_collapsed_size); - - updateSettingsAnimator(); - } - - private void updateSettingsAnimator() { - mSettingsAlpha = createSettingsAlphaAnimator(); - - final boolean isRtl = isLayoutRtl(); - if (isRtl && mDate.getWidth() == 0) { - mDate.addOnLayoutChangeListener(new OnLayoutChangeListener() { - @Override - public void onLayoutChange(View v, int left, int top, int right, int bottom, - int oldLeft, int oldTop, int oldRight, int oldBottom) { - mDate.setPivotX(getWidth()); - mDate.removeOnLayoutChangeListener(this); - } - }); - } else { - mDate.setPivotX(isRtl ? mDate.getWidth() : 0); - } - } +/** + * The bottom footer of the quick settings panel. + */ +public interface QSFooter { + /** + * Sets the given {@link QSPanel} to be the one that will display the quick settings. + */ + void setQSPanel(@Nullable QSPanel panel); + + /** + * Sets whether or not the footer should be visible. + * + * @param visibility One of {@link View#VISIBLE}, {@link View#INVISIBLE} or {@link View#GONE}. + * @see View#setVisibility(int) + */ + void setVisibility(int visibility); + + /** + * Sets whether the footer is in an expanded state. + */ + void setExpanded(boolean expanded); + + /** + * Returns the full height of the footer. + */ + int getHeight(); + + /** + * Sets the percentage amount that the quick settings has been expanded. + * + * @param expansion A value from 1 to 0 that indicates how much the quick settings have been + * expanded. 1 is fully expanded. + */ + void setExpansion(float expansion); + + /** + * Sets whether or not this footer should set itself to listen for changes in any callbacks + * that it has implemented. + */ + void setListening(boolean listening); + + /** + * Sets whether or not the keyguard is currently being shown. + */ + void setKeyguardShowing(boolean keyguardShowing); + + /** + * Returns the {@link View} that should expand the quick settings when clicked. + */ @Nullable - private TouchAnimator createSettingsAlphaAnimator() { - // If the settings icon is not shown and the user switcher is always shown, then there - // is nothing to animate. - if (!mShowEditIcon && mAlwaysShowMultiUserSwitch) { - return null; - } - - TouchAnimator.Builder animatorBuilder = new TouchAnimator.Builder(); - animatorBuilder.setStartDelay(QSAnimator.EXPANDED_TILE_DELAY); - - if (mShowEditIcon) { - animatorBuilder.addFloat(mEdit, "alpha", 0, 1); - } - - if (!mAlwaysShowMultiUserSwitch) { - animatorBuilder.addFloat(mMultiUserSwitch, "alpha", 0, 1); - } - - return animatorBuilder.build(); - } - - public void setKeyguardShowing(boolean keyguardShowing) { - mKeyguardShowing = keyguardShowing; - setExpansion(mExpansionAmount); - } - - public void setExpanded(boolean expanded) { - if (mExpanded == expanded) return; - mExpanded = expanded; - updateEverything(); - } - - @Override - public void onNextAlarmChanged(AlarmManager.AlarmClockInfo nextAlarm) { - mNextAlarm = nextAlarm; - if (nextAlarm != null) { - String alarmString = KeyguardStatusView.formatNextAlarm(getContext(), nextAlarm); - mAlarmStatus.setText(alarmString); - mAlarmStatus.setContentDescription(mContext.getString( - R.string.accessibility_quick_settings_alarm, alarmString)); - mAlarmStatusCollapsed.setContentDescription(mContext.getString( - R.string.accessibility_quick_settings_alarm, alarmString)); - } - if (mAlarmShowing != (nextAlarm != null)) { - mAlarmShowing = nextAlarm != null; - updateAnimator(getWidth()); - updateEverything(); - } - } - - public void setExpansion(float headerExpansionFraction) { - mExpansionAmount = headerExpansionFraction; - if (mAnimator != null) mAnimator.setPosition(headerExpansionFraction); - if (mAlarmAnimator != null) mAlarmAnimator.setPosition( - mKeyguardShowing ? 0 : headerExpansionFraction); - - if (mSettingsAlpha != null) { - mSettingsAlpha.setPosition(headerExpansionFraction); - } - - updateAlarmVisibilities(); - - mExpandIndicator.setExpanded(headerExpansionFraction > EXPAND_INDICATOR_THRESHOLD); - } - - @Override - @VisibleForTesting - public void onDetachedFromWindow() { - setListening(false); - super.onDetachedFromWindow(); - } - - private void updateAlarmVisibilities() { - mAlarmStatusCollapsed.setVisibility(mAlarmShowing ? View.VISIBLE : View.GONE); - } - - public void setListening(boolean listening) { - if (listening == mListening) { - return; - } - mListening = listening; - updateListeners(); - } - - public View getExpandView() { - return findViewById(R.id.expand_indicator); - } - - public void updateEverything() { - post(() -> { - updateVisibilities(); - setClickable(false); - }); - } - - private void updateVisibilities() { - updateAlarmVisibilities(); - mSettingsContainer.findViewById(R.id.tuner_icon).setVisibility( - TunerService.isTunerEnabled(mContext) ? View.VISIBLE : View.INVISIBLE); - final boolean isDemo = UserManager.isDeviceInDemoMode(mContext); - - mMultiUserSwitch.setVisibility((mExpanded || mAlwaysShowMultiUserSwitch) - && mMultiUserSwitch.hasMultipleUsers() && !isDemo - ? View.VISIBLE : View.INVISIBLE); - - if (mShowEditIcon) { - mEdit.setVisibility(isDemo || !mExpanded ? View.INVISIBLE : View.VISIBLE); - } - } - - private void updateListeners() { - if (mListening) { - mNextAlarmController.addCallback(this); - mUserInfoController.addCallback(this); - if (Dependency.get(NetworkController.class).hasVoiceCallingFeature()) { - Dependency.get(NetworkController.class).addEmergencyListener(this); - Dependency.get(NetworkController.class).addCallback(this); - } - } else { - mNextAlarmController.removeCallback(this); - mUserInfoController.removeCallback(this); - Dependency.get(NetworkController.class).removeEmergencyListener(this); - Dependency.get(NetworkController.class).removeCallback(this); - } - } - - public void setQSPanel(final QSPanel qsPanel) { - mQsPanel = qsPanel; - if (mQsPanel != null) { - mMultiUserSwitch.setQsPanel(qsPanel); - } - } - - @Override - public void onClick(View v) { - if (v == mSettingsButton) { - if (!Dependency.get(DeviceProvisionedController.class).isCurrentUserSetup()) { - // If user isn't setup just unlock the device and dump them back at SUW. - mActivityStarter.postQSRunnableDismissingKeyguard(() -> { }); - return; - } - MetricsLogger.action(mContext, - mExpanded ? MetricsProto.MetricsEvent.ACTION_QS_EXPANDED_SETTINGS_LAUNCH - : MetricsProto.MetricsEvent.ACTION_QS_COLLAPSED_SETTINGS_LAUNCH); - if (mSettingsButton.isTunerClick()) { - Dependency.get(ActivityStarter.class).postQSRunnableDismissingKeyguard(() -> { - if (TunerService.isTunerEnabled(mContext)) { - TunerService.showResetRequest(mContext, () -> { - // Relaunch settings so that the tuner disappears. - startSettingsActivity(); - }); - } else { - Toast.makeText(getContext(), R.string.tuner_toast, - Toast.LENGTH_LONG).show(); - TunerService.setTunerEnabled(mContext, true); - } - startSettingsActivity(); - - }); - } else { - startSettingsActivity(); - } - } else if (v == mDateTimeGroup) { - Dependency.get(MetricsLogger.class).action(ACTION_QS_DATE, - mNextAlarm != null); - if (mNextAlarm != null) { - PendingIntent showIntent = mNextAlarm.getShowIntent(); - mActivityStarter.startPendingIntentDismissingKeyguard(showIntent); - } else { - mActivityStarter.postStartActivityDismissingKeyguard(new Intent( - AlarmClock.ACTION_SHOW_ALARMS), 0); - } - } - } - - private void startSettingsActivity() { - mActivityStarter.startActivity(new Intent(android.provider.Settings.ACTION_SETTINGS), - true /* dismissShade */); - } - - @Override - public void setEmergencyCallsOnly(boolean show) { - boolean changed = show != mShowEmergencyCallsOnly; - if (changed) { - mShowEmergencyCallsOnly = show; - if (mExpanded) { - updateEverything(); - } - } - } - - @Override - public void onUserInfoChanged(String name, Drawable picture, String userAccount) { - if (picture != null && - UserManager.get(mContext).isGuestUser(ActivityManager.getCurrentUser())) { - picture = picture.getConstantState().newDrawable().mutate(); - picture.setColorFilter( - Utils.getColorAttr(mContext, android.R.attr.colorForeground), - Mode.SRC_IN); - } - mMultiUserAvatar.setImageDrawable(picture); - } + View getExpandView(); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFooterImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSFooterImpl.java new file mode 100644 index 000000000000..94da5f72be10 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/QSFooterImpl.java @@ -0,0 +1,414 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.systemui.qs; + +import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.ACTION_QS_DATE; + +import android.app.ActivityManager; +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.PorterDuff.Mode; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.RippleDrawable; +import android.os.UserManager; +import android.provider.AlarmClock; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; +import android.util.AttributeSet; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; + +import com.android.internal.logging.MetricsLogger; +import com.android.internal.logging.nano.MetricsProto; +import com.android.keyguard.KeyguardStatusView; +import com.android.settingslib.Utils; +import com.android.systemui.Dependency; +import com.android.systemui.FontSizeUtils; +import com.android.systemui.R; +import com.android.systemui.R.dimen; +import com.android.systemui.R.id; +import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.qs.TouchAnimator.Builder; +import com.android.systemui.qs.TouchAnimator.Listener; +import com.android.systemui.qs.TouchAnimator.ListenerAdapter; +import com.android.systemui.statusbar.phone.ExpandableIndicator; +import com.android.systemui.statusbar.phone.MultiUserSwitch; +import com.android.systemui.statusbar.phone.SettingsButton; +import com.android.systemui.statusbar.policy.DeviceProvisionedController; +import com.android.systemui.statusbar.policy.NetworkController; +import com.android.systemui.statusbar.policy.NetworkController.EmergencyListener; +import com.android.systemui.statusbar.policy.NetworkController.SignalCallback; +import com.android.systemui.statusbar.policy.NextAlarmController; +import com.android.systemui.statusbar.policy.NextAlarmController.NextAlarmChangeCallback; +import com.android.systemui.statusbar.policy.UserInfoController; +import com.android.systemui.statusbar.policy.UserInfoController.OnUserInfoChangedListener; +import com.android.systemui.tuner.TunerService; + +public class QSFooterImpl extends FrameLayout implements QSFooter, + NextAlarmChangeCallback, OnClickListener, OnUserInfoChangedListener, EmergencyListener, + SignalCallback { + private static final float EXPAND_INDICATOR_THRESHOLD = .93f; + + private ActivityStarter mActivityStarter; + private NextAlarmController mNextAlarmController; + private UserInfoController mUserInfoController; + private SettingsButton mSettingsButton; + protected View mSettingsContainer; + + private TextView mAlarmStatus; + private View mAlarmStatusCollapsed; + private View mDate; + + private QSPanel mQsPanel; + + private boolean mExpanded; + private boolean mAlarmShowing; + + protected ExpandableIndicator mExpandIndicator; + + private boolean mListening; + private AlarmManager.AlarmClockInfo mNextAlarm; + + private boolean mShowEmergencyCallsOnly; + protected MultiUserSwitch mMultiUserSwitch; + private ImageView mMultiUserAvatar; + + protected TouchAnimator mSettingsAlpha; + private float mExpansionAmount; + + protected View mEdit; + private TouchAnimator mAnimator; + private View mDateTimeGroup; + private boolean mKeyguardShowing; + private TouchAnimator mAlarmAnimator; + + public QSFooterImpl(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + Resources res = getResources(); + + mEdit = findViewById(android.R.id.edit); + mEdit.setOnClickListener(view -> + Dependency.get(ActivityStarter.class).postQSRunnableDismissingKeyguard(() -> + mQsPanel.showEdit(view))); + + mDateTimeGroup = findViewById(id.date_time_alarm_group); + mDate = findViewById(R.id.date); + + mExpandIndicator = findViewById(R.id.expand_indicator); + mSettingsButton = findViewById(R.id.settings_button); + mSettingsContainer = findViewById(R.id.settings_button_container); + mSettingsButton.setOnClickListener(this); + + mAlarmStatusCollapsed = findViewById(R.id.alarm_status_collapsed); + mAlarmStatus = findViewById(R.id.alarm_status); + mDateTimeGroup.setOnClickListener(this); + + mMultiUserSwitch = findViewById(R.id.multi_user_switch); + mMultiUserAvatar = mMultiUserSwitch.findViewById(R.id.multi_user_avatar); + + // RenderThread is doing more harm than good when touching the header (to expand quick + // settings), so disable it for this view + ((RippleDrawable) mSettingsButton.getBackground()).setForceSoftware(true); + ((RippleDrawable) mExpandIndicator.getBackground()).setForceSoftware(true); + + updateResources(); + + mNextAlarmController = Dependency.get(NextAlarmController.class); + mUserInfoController = Dependency.get(UserInfoController.class); + mActivityStarter = Dependency.get(ActivityStarter.class); + addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop, oldRight, + oldBottom) -> updateAnimator(right - left)); + } + + private void updateAnimator(int width) { + int numTiles = QuickQSPanel.getNumQuickTiles(mContext); + int size = mContext.getResources().getDimensionPixelSize(R.dimen.qs_quick_tile_size) + - mContext.getResources().getDimensionPixelSize(dimen.qs_quick_tile_padding); + int remaining = (width - numTiles * size) / (numTiles - 1); + int defSpace = mContext.getResources().getDimensionPixelOffset(R.dimen.default_gear_space); + + mAnimator = new Builder() + .addFloat(mSettingsContainer, "translationX", -(remaining - defSpace), 0) + .addFloat(mSettingsButton, "rotation", -120, 0) + .build(); + if (mAlarmShowing) { + mAlarmAnimator = new Builder().addFloat(mDate, "alpha", 1, 0) + .addFloat(mDateTimeGroup, "translationX", 0, -mDate.getWidth()) + .addFloat(mAlarmStatus, "alpha", 0, 1) + .setListener(new ListenerAdapter() { + @Override + public void onAnimationAtStart() { + mAlarmStatus.setVisibility(View.GONE); + } + + @Override + public void onAnimationStarted() { + mAlarmStatus.setVisibility(View.VISIBLE); + } + }).build(); + } else { + mAlarmAnimator = null; + mAlarmStatus.setVisibility(View.GONE); + mDate.setAlpha(1); + mDateTimeGroup.setTranslationX(0); + } + setExpansion(mExpansionAmount); + } + + @Override + protected void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + updateResources(); + } + + @Override + public void onRtlPropertiesChanged(int layoutDirection) { + super.onRtlPropertiesChanged(layoutDirection); + updateResources(); + } + + private void updateResources() { + FontSizeUtils.updateFontSize(mAlarmStatus, R.dimen.qs_date_collapsed_size); + + updateSettingsAnimator(); + } + + private void updateSettingsAnimator() { + mSettingsAlpha = createSettingsAlphaAnimator(); + + final boolean isRtl = isLayoutRtl(); + if (isRtl && mDate.getWidth() == 0) { + mDate.addOnLayoutChangeListener(new OnLayoutChangeListener() { + @Override + public void onLayoutChange(View v, int left, int top, int right, int bottom, + int oldLeft, int oldTop, int oldRight, int oldBottom) { + mDate.setPivotX(getWidth()); + mDate.removeOnLayoutChangeListener(this); + } + }); + } else { + mDate.setPivotX(isRtl ? mDate.getWidth() : 0); + } + } + + @Nullable + private TouchAnimator createSettingsAlphaAnimator() { + return new TouchAnimator.Builder() + .addFloat(mEdit, "alpha", 0, 1) + .addFloat(mMultiUserSwitch, "alpha", 0, 1) + .build(); + } + + @Override + public void setKeyguardShowing(boolean keyguardShowing) { + mKeyguardShowing = keyguardShowing; + setExpansion(mExpansionAmount); + } + + @Override + public void setExpanded(boolean expanded) { + if (mExpanded == expanded) return; + mExpanded = expanded; + updateEverything(); + } + + @Override + public void onNextAlarmChanged(AlarmManager.AlarmClockInfo nextAlarm) { + mNextAlarm = nextAlarm; + if (nextAlarm != null) { + String alarmString = KeyguardStatusView.formatNextAlarm(getContext(), nextAlarm); + mAlarmStatus.setText(alarmString); + mAlarmStatus.setContentDescription(mContext.getString( + R.string.accessibility_quick_settings_alarm, alarmString)); + mAlarmStatusCollapsed.setContentDescription(mContext.getString( + R.string.accessibility_quick_settings_alarm, alarmString)); + } + if (mAlarmShowing != (nextAlarm != null)) { + mAlarmShowing = nextAlarm != null; + updateAnimator(getWidth()); + updateEverything(); + } + } + + @Override + public void setExpansion(float headerExpansionFraction) { + mExpansionAmount = headerExpansionFraction; + if (mAnimator != null) mAnimator.setPosition(headerExpansionFraction); + if (mAlarmAnimator != null) mAlarmAnimator.setPosition( + mKeyguardShowing ? 0 : headerExpansionFraction); + + if (mSettingsAlpha != null) { + mSettingsAlpha.setPosition(headerExpansionFraction); + } + + updateAlarmVisibilities(); + + mExpandIndicator.setExpanded(headerExpansionFraction > EXPAND_INDICATOR_THRESHOLD); + } + + @Override + @VisibleForTesting + public void onDetachedFromWindow() { + setListening(false); + super.onDetachedFromWindow(); + } + + private void updateAlarmVisibilities() { + mAlarmStatusCollapsed.setVisibility(mAlarmShowing ? View.VISIBLE : View.GONE); + } + + @Override + public void setListening(boolean listening) { + if (listening == mListening) { + return; + } + mListening = listening; + updateListeners(); + } + + @Override + public View getExpandView() { + return findViewById(R.id.expand_indicator); + } + + public void updateEverything() { + post(() -> { + updateVisibilities(); + setClickable(false); + }); + } + + private void updateVisibilities() { + updateAlarmVisibilities(); + mSettingsContainer.findViewById(R.id.tuner_icon).setVisibility( + TunerService.isTunerEnabled(mContext) ? View.VISIBLE : View.INVISIBLE); + final boolean isDemo = UserManager.isDeviceInDemoMode(mContext); + + mMultiUserSwitch.setVisibility(mExpanded && mMultiUserSwitch.hasMultipleUsers() && !isDemo + ? View.VISIBLE : View.INVISIBLE); + + mEdit.setVisibility(isDemo || !mExpanded ? View.INVISIBLE : View.VISIBLE); + } + + private void updateListeners() { + if (mListening) { + mNextAlarmController.addCallback(this); + mUserInfoController.addCallback(this); + if (Dependency.get(NetworkController.class).hasVoiceCallingFeature()) { + Dependency.get(NetworkController.class).addEmergencyListener(this); + Dependency.get(NetworkController.class).addCallback(this); + } + } else { + mNextAlarmController.removeCallback(this); + mUserInfoController.removeCallback(this); + Dependency.get(NetworkController.class).removeEmergencyListener(this); + Dependency.get(NetworkController.class).removeCallback(this); + } + } + + @Override + public void setQSPanel(final QSPanel qsPanel) { + mQsPanel = qsPanel; + if (mQsPanel != null) { + mMultiUserSwitch.setQsPanel(qsPanel); + } + } + + @Override + public void onClick(View v) { + if (v == mSettingsButton) { + if (!Dependency.get(DeviceProvisionedController.class).isCurrentUserSetup()) { + // If user isn't setup just unlock the device and dump them back at SUW. + mActivityStarter.postQSRunnableDismissingKeyguard(() -> { }); + return; + } + MetricsLogger.action(mContext, + mExpanded ? MetricsProto.MetricsEvent.ACTION_QS_EXPANDED_SETTINGS_LAUNCH + : MetricsProto.MetricsEvent.ACTION_QS_COLLAPSED_SETTINGS_LAUNCH); + if (mSettingsButton.isTunerClick()) { + Dependency.get(ActivityStarter.class).postQSRunnableDismissingKeyguard(() -> { + if (TunerService.isTunerEnabled(mContext)) { + TunerService.showResetRequest(mContext, () -> { + // Relaunch settings so that the tuner disappears. + startSettingsActivity(); + }); + } else { + Toast.makeText(getContext(), R.string.tuner_toast, + Toast.LENGTH_LONG).show(); + TunerService.setTunerEnabled(mContext, true); + } + startSettingsActivity(); + + }); + } else { + startSettingsActivity(); + } + } else if (v == mDateTimeGroup) { + Dependency.get(MetricsLogger.class).action(ACTION_QS_DATE, + mNextAlarm != null); + if (mNextAlarm != null) { + PendingIntent showIntent = mNextAlarm.getShowIntent(); + mActivityStarter.startPendingIntentDismissingKeyguard(showIntent); + } else { + mActivityStarter.postStartActivityDismissingKeyguard(new Intent( + AlarmClock.ACTION_SHOW_ALARMS), 0); + } + } + } + + private void startSettingsActivity() { + mActivityStarter.startActivity(new Intent(android.provider.Settings.ACTION_SETTINGS), + true /* dismissShade */); + } + + @Override + public void setEmergencyCallsOnly(boolean show) { + boolean changed = show != mShowEmergencyCallsOnly; + if (changed) { + mShowEmergencyCallsOnly = show; + if (mExpanded) { + updateEverything(); + } + } + } + + @Override + public void onUserInfoChanged(String name, Drawable picture, String userAccount) { + if (picture != null && + UserManager.get(mContext).isGuestUser(ActivityManager.getCurrentUser())) { + picture = picture.getConstantState().newDrawable().mutate(); + picture.setColorFilter( + Utils.getColorAttr(mContext, android.R.attr.colorForeground), + Mode.SRC_IN); + } + mMultiUserAvatar.setImageDrawable(picture); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java index bb3672511c48..f9ccb50dfb41 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java @@ -16,11 +16,11 @@ package com.android.systemui.qs; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; -import android.annotation.Nullable; import android.app.Fragment; import android.content.res.Configuration; import android.graphics.Rect; import android.os.Bundle; +import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import android.util.Log; import android.view.ContextThemeWrapper; @@ -80,14 +80,9 @@ public class QSFragment extends Fragment implements QS { mFooter = view.findViewById(R.id.qs_footer); mContainer = view.findViewById(id.quick_settings_container); - mQSDetail.setQsPanel(mQSPanel, mHeader, mFooter); - - // If the quick settings row is not shown, then there is no need for the animation from - // the row to the full QS panel. - if (getResources().getBoolean(R.bool.config_showQuickSettingsRow)) { - mQSAnimator = new QSAnimator(this, - mHeader.findViewById(R.id.quick_qs_panel), mQSPanel); - } + mQSDetail.setQsPanel(mQSPanel, mHeader, (View) mFooter); + mQSAnimator = new QSAnimator(this, + mHeader.findViewById(R.id.quick_qs_panel), mQSPanel); mQSCustomizer = view.findViewById(R.id.qs_customize); mQSCustomizer.setQs(this); @@ -131,6 +126,7 @@ public class QSFragment extends Fragment implements QS { public void setHasNotifications(boolean hasNotifications) { } + @Override public void setPanelView(HeightListener panelView) { mPanelView = panelView; } @@ -154,6 +150,7 @@ public class QSFragment extends Fragment implements QS { } } + @Override public boolean isCustomizing() { return mQSCustomizer.isCustomizing(); } @@ -195,15 +192,22 @@ public class QSFragment extends Fragment implements QS { return mQSCustomizer; } + @Override public boolean isShowingDetail() { return mQSPanel.isShowingCustomize() || mQSDetail.isShowingDetail(); } + @Override public void setHeaderClickable(boolean clickable) { if (DEBUG) Log.d(TAG, "setHeaderClickable " + clickable); - mFooter.getExpandView().setClickable(clickable); + + View expandView = mFooter.getExpandView(); + if (expandView != null) { + expandView.setClickable(clickable); + } } + @Override public void setExpanded(boolean expanded) { if (DEBUG) Log.d(TAG, "setExpanded " + expanded); mQsExpanded = expanded; @@ -211,6 +215,7 @@ public class QSFragment extends Fragment implements QS { updateQsState(); } + @Override public void setKeyguardShowing(boolean keyguardShowing) { if (DEBUG) Log.d(TAG, "setKeyguardShowing " + keyguardShowing); mKeyguardShowing = keyguardShowing; @@ -223,12 +228,14 @@ public class QSFragment extends Fragment implements QS { updateQsState(); } + @Override public void setOverscrolling(boolean stackScrollerOverscrolling) { if (DEBUG) Log.d(TAG, "setOverscrolling " + stackScrollerOverscrolling); mStackScrollerOverscrolling = stackScrollerOverscrolling; updateQsState(); } + @Override public void setListening(boolean listening) { if (DEBUG) Log.d(TAG, "setListening " + listening); mListening = listening; @@ -237,11 +244,13 @@ public class QSFragment extends Fragment implements QS { mQSPanel.setListening(mListening && mQsExpanded); } + @Override public void setHeaderListening(boolean listening) { mHeader.setListening(listening); mFooter.setListening(listening); } + @Override public void setQsExpansion(float expansion, float headerTranslation) { if (DEBUG) Log.d(TAG, "setQSExpansion " + expansion + " " + headerTranslation); mContainer.setExpansion(expansion); @@ -269,6 +278,7 @@ public class QSFragment extends Fragment implements QS { mQSPanel.setClipBounds(mQsBounds); } + @Override public void animateHeaderSlidingIn(long delay) { if (DEBUG) Log.d(TAG, "animateHeaderSlidingIn"); // If the QS is already expanded we don't need to slide in the header as it's already @@ -280,6 +290,7 @@ public class QSFragment extends Fragment implements QS { } } + @Override public void animateHeaderSlidingOut() { if (DEBUG) Log.d(TAG, "animateHeaderSlidingOut"); mHeaderAnimating = true; @@ -300,7 +311,11 @@ public class QSFragment extends Fragment implements QS { @Override public void setExpandClickListener(OnClickListener onClickListener) { - mFooter.getExpandView().setOnClickListener(onClickListener); + View expandView = mFooter.getExpandView(); + + if (expandView != null) { + expandView.setOnClickListener(onClickListener); + } } @Override @@ -323,6 +338,7 @@ public class QSFragment extends Fragment implements QS { * The height this view wants to be. This is different from {@link #getMeasuredHeight} such that * during closing the detail panel, this already returns the smaller height. */ + @Override public int getDesiredHeight() { if (mQSCustomizer.isCustomizing()) { return getView().getHeight(); @@ -342,6 +358,7 @@ public class QSFragment extends Fragment implements QS { mContainer.setHeightOverride(desiredHeight); } + @Override public int getQsMinExpansionHeight() { return mHeader.getHeight(); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java index 7ec07604fab5..0709e229bd4d 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java @@ -58,8 +58,6 @@ public class QuickStatusBarHeader extends RelativeLayout { Resources res = getResources(); mHeaderQsPanel = findViewById(R.id.quick_qs_panel); - mHeaderQsPanel.setVisibility(res.getBoolean(R.bool.config_showQuickSettingsRow) - ? VISIBLE : GONE); // RenderThread is doing more harm than good when touching the header (to expand quick // settings), so disable it for this view diff --git a/packages/SystemUI/src/com/android/systemui/qs/car/CarQSFooter.java b/packages/SystemUI/src/com/android/systemui/qs/car/CarQSFooter.java new file mode 100644 index 000000000000..9730f29da977 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/car/CarQSFooter.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package com.android.systemui.qs.car; + +import android.content.Context; +import android.content.Intent; +import android.graphics.drawable.Drawable; +import android.support.annotation.Nullable; +import android.util.AttributeSet; +import android.view.View; +import android.widget.ImageView; +import android.widget.RelativeLayout; + +import com.android.systemui.Dependency; +import com.android.systemui.R; +import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.qs.QSFooter; +import com.android.systemui.qs.QSPanel; +import com.android.systemui.statusbar.phone.MultiUserSwitch; +import com.android.systemui.statusbar.policy.DeviceProvisionedController; +import com.android.systemui.statusbar.policy.UserInfoController; + +/** + * The footer view that displays below the status bar in the auto use-case. This view shows the + * user switcher and access to settings. + */ +public class CarQSFooter extends RelativeLayout implements QSFooter, + UserInfoController.OnUserInfoChangedListener { + private UserInfoController mUserInfoController; + + private MultiUserSwitch mMultiUserSwitch; + private ImageView mMultiUserAvatar; + + public CarQSFooter(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mMultiUserSwitch = findViewById(R.id.multi_user_switch); + mMultiUserAvatar = mMultiUserSwitch.findViewById(R.id.multi_user_avatar); + + mUserInfoController = Dependency.get(UserInfoController.class); + + findViewById(R.id.settings_button).setOnClickListener(v -> { + ActivityStarter activityStarter = Dependency.get(ActivityStarter.class); + + if (!Dependency.get(DeviceProvisionedController.class).isCurrentUserSetup()) { + // If user isn't setup just unlock the device and dump them back at SUW. + activityStarter.postQSRunnableDismissingKeyguard(() -> { }); + return; + } + + activityStarter.startActivity(new Intent(android.provider.Settings.ACTION_SETTINGS), + true /* dismissShade */); + }); + } + + @Override + public void onUserInfoChanged(String name, Drawable picture, String userAccount) { + mMultiUserAvatar.setImageDrawable(picture); + } + + @Override + public void setQSPanel(@Nullable QSPanel panel) { + if (panel != null) { + mMultiUserSwitch.setQsPanel(panel); + } + } + + @Override + public void setListening(boolean listening) { + if (listening) { + mUserInfoController.addCallback(this); + } else { + mUserInfoController.removeCallback(this); + } + } + + @Nullable + @Override + public View getExpandView() { + // No view that should expand/collapse the quick settings. + return null; + } + + @Override + public void setExpanded(boolean expanded) { + // Do nothing because the quick settings cannot be expanded. + } + + @Override + public void setExpansion(float expansion) { + // Do nothing because the quick settings cannot be expanded. + } + + @Override + public void setKeyguardShowing(boolean keyguardShowing) { + // Do nothing because the footer will not be shown when the keyguard is up. + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/car/CarQSFragment.java b/packages/SystemUI/src/com/android/systemui/qs/car/CarQSFragment.java new file mode 100644 index 000000000000..7c2a8129813a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/car/CarQSFragment.java @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package com.android.systemui.qs.car; + +import android.app.Fragment; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; + +import com.android.systemui.R; +import com.android.systemui.plugins.qs.QS; +import com.android.systemui.qs.QSFooter; + +/** + * A quick settings fragment for the car. For auto, there is no row for quick settings or ability + * to expand the quick settings panel. Instead, the only thing is that displayed is the + * status bar, and a static row with access to the user switcher and settings. + */ +public class CarQSFragment extends Fragment implements QS { + private View mHeader; + private QSFooter mFooter; + + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, + Bundle savedInstanceState) { + return inflater.inflate(R.layout.car_qs_panel, container, false); + } + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + mHeader = view.findViewById(R.id.header); + mFooter = view.findViewById(R.id.qs_footer); + } + + @Override + public void hideImmediately() { + getView().setVisibility(View.INVISIBLE); + } + + @Override + public void setQsExpansion(float qsExpansionFraction, float headerTranslation) { + // If the header is to be completed translated down, then set it to be visible. + getView().setVisibility(headerTranslation == 0 ? View.VISIBLE : View.INVISIBLE); + } + + @Override + public View getHeader() { + return mHeader; + } + + @VisibleForTesting + QSFooter getFooter() { + return mFooter; + } + + @Override + public void setHeaderListening(boolean listening) { + mFooter.setListening(listening); + } + + @Override + public void setListening(boolean listening) { + mFooter.setListening(listening); + } + + @Override + public int getQsMinExpansionHeight() { + return getView().getHeight(); + } + + @Override + public int getDesiredHeight() { + return getView().getHeight(); + } + + @Override + public void setPanelView(HeightListener notificationPanelView) { + // No quick settings panel. + } + + @Override + public void setHeightOverride(int desiredHeight) { + // No ability to expand quick settings. + } + + @Override + public void setHeaderClickable(boolean qsExpansionEnabled) { + // Usually this sets the expand button to be clickable, but there is no quick settings to + // expand. + } + + @Override + public boolean isCustomizing() { + // No ability to customize the quick settings. + return false; + } + + @Override + public void setOverscrolling(boolean overscrolling) { + // No overscrolling to reveal quick settings. + } + + @Override + public void setExpanded(boolean qsExpanded) { + // No quick settings to expand + } + + @Override + public boolean isShowingDetail() { + // No detail panel to close. + return false; + } + + @Override + public void closeDetail() { + // No detail panel to close. + } + + @Override + public void setKeyguardShowing(boolean keyguardShowing) { + // No keyguard to show. + } + + @Override + public void animateHeaderSlidingIn(long delay) { + // No header to animate. + } + + @Override + public void animateHeaderSlidingOut() { + // No header to animate. + } + + @Override + public void notifyCustomizeChanged() { + // There is no ability to customize quick settings. + } + + @Override + public void setContainer(ViewGroup container) { + // No quick settings, so no container to set. + } + + @Override + public void setExpandClickListener(OnClickListener onClickListener) { + // No ability to expand the quick settings. + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/car/CarStatusBarHeader.java b/packages/SystemUI/src/com/android/systemui/qs/car/CarStatusBarHeader.java new file mode 100644 index 000000000000..6797bb9dbe82 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/car/CarStatusBarHeader.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package com.android.systemui.qs.car; + +import android.content.Context; +import android.graphics.Color; +import android.graphics.Rect; +import android.support.annotation.IdRes; +import android.util.AttributeSet; +import android.view.View; +import android.widget.RelativeLayout; + +import com.android.settingslib.Utils; +import com.android.systemui.BatteryMeterView; +import com.android.systemui.R; +import com.android.systemui.statusbar.policy.DarkIconDispatcher; + +/** + * A view that forms the header of the notification panel. This view will ensure that any + * status icons that are displayed are tinted accordingly to the current theme. + */ +public class CarStatusBarHeader extends RelativeLayout { + public CarStatusBarHeader(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + // Set the light/dark theming on the header status UI to match the current theme. + int colorForeground = Utils.getColorAttr(getContext(), android.R.attr.colorForeground); + float intensity = colorForeground == Color.WHITE ? 0f : 1f; + Rect tintArea = new Rect(0, 0, 0, 0); + + applyDarkness(R.id.signal_cluster, tintArea, intensity, colorForeground); + applyDarkness(R.id.battery, tintArea, intensity, colorForeground); + applyDarkness(R.id.clock, tintArea, intensity, colorForeground); + + ((BatteryMeterView) findViewById(R.id.battery)).setForceShowPercent(true); + } + + private void applyDarkness(@IdRes int id, Rect tintArea, float intensity, int color) { + View v = findViewById(id); + if (v instanceof DarkIconDispatcher.DarkReceiver) { + ((DarkIconDispatcher.DarkReceiver) v).onDarkChanged(tintArea, intensity, color); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recents/Recents.java b/packages/SystemUI/src/com/android/systemui/recents/Recents.java index f7663fc14fe2..6b8570771175 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/Recents.java +++ b/packages/SystemUI/src/com/android/systemui/recents/Recents.java @@ -43,11 +43,14 @@ import android.widget.Toast; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.systemui.Dependency; import com.android.systemui.EventLogConstants; import com.android.systemui.EventLogTags; import com.android.systemui.R; import com.android.systemui.RecentsComponent; import com.android.systemui.SystemUI; +import com.android.systemui.plugins.PluginActivity; +import com.android.systemui.plugins.PluginActivityManager; import com.android.systemui.recents.events.EventBus; import com.android.systemui.recents.events.activity.ConfigurationChangedEvent; import com.android.systemui.recents.events.activity.DockedTopTaskEvent; @@ -234,6 +237,8 @@ public class Recents extends SystemUI registerWithSystemUser(); } putComponent(Recents.class, this); + Dependency.get(PluginActivityManager.class).addActivityPlugin(RecentsImpl.RECENTS_ACTIVITY, + PluginActivity.ACTION_RECENTS); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java b/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java index 7b574833cd47..a35310fc7d59 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java @@ -46,6 +46,7 @@ import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.os.Environment; +import android.os.PowerManager; import android.os.Process; import android.os.UserHandle; import android.provider.MediaStore; @@ -61,6 +62,7 @@ import android.view.ViewGroup; import android.view.WindowManager; import android.view.animation.Interpolator; import android.widget.ImageView; +import android.widget.Toast; import com.android.internal.messages.nano.SystemMessageProto.SystemMessage; import com.android.systemui.R; @@ -678,6 +680,13 @@ class GlobalScreenshot { */ private void startAnimation(final Runnable finisher, int w, int h, boolean statusBarVisible, boolean navBarVisible) { + // If power save is on, show a toast so there is some visual indication that a screenshot + // has been taken. + PowerManager powerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); + if (powerManager.isPowerSaveMode()) { + Toast.makeText(mContext, R.string.screenshot_saved_title, Toast.LENGTH_SHORT).show(); + } + // Add the view for the animation mScreenshotView.setImageBitmap(mScreenBitmap); mScreenshotLayout.requestFocus(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java index 97e2f6d3e6f1..7abceaf41c70 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java @@ -16,15 +16,18 @@ package com.android.systemui.statusbar; +import static com.android.systemui.statusbar.phone.NotificationIconContainer.IconState.NO_VALUE; +import static com.android.systemui.statusbar.phone.NotificationIconContainer.OVERFLOW_EARLY_AMOUNT; + import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; import android.os.SystemProperties; import android.util.AttributeSet; -import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.view.accessibility.AccessibilityNodeInfo; + import com.android.systemui.Interpolators; import com.android.systemui.R; import com.android.systemui.ViewInvertHelper; @@ -315,26 +318,65 @@ public class NotificationShelf extends ActivatableNotificationView implements private float updateIconAppearance(ExpandableNotificationRow row, float expandAmount, boolean scrolling, boolean scrollingFast, boolean expandingAnimated, boolean isLastChild) { + StatusBarIconView icon = row.getEntry().expandedIcon; + NotificationIconContainer.IconState iconState = getIconState(icon); + if (iconState == null) { + return 0.0f; + } + // Let calculate how much the view is in the shelf float viewStart = row.getTranslationY(); int fullHeight = row.getActualHeight() + mPaddingBetweenElements; float iconTransformDistance = getIntrinsicHeight() * 1.5f; iconTransformDistance *= NotificationUtils.interpolate(1.f, 1.5f, expandAmount); + iconTransformDistance = Math.min(iconTransformDistance, fullHeight); if (isLastChild) { fullHeight = Math.min(fullHeight, row.getMinHeight() - getIntrinsicHeight()); iconTransformDistance = Math.min(iconTransformDistance, row.getMinHeight() - getIntrinsicHeight()); } float viewEnd = viewStart + fullHeight; + if (expandingAnimated && mAmbientState.getScrollY() == 0 + && !mAmbientState.isOnKeyguard() && !iconState.isLastExpandIcon) { + // We are expanding animated. Because we switch to a linear interpolation in this case, + // the last icon may be stuck in between the shelf position and the notification + // position, which looks pretty bad. We therefore optimize this case by applying a + // shorter transition such that the icon is either fully in the notification or we clamp + // it into the shelf if it's close enough. + // We need to persist this, since after the expansion, the behavior should still be the + // same. + float position = mAmbientState.getIntrinsicPadding() + + mHostLayout.getPositionInLinearLayout(row); + int maxShelfStart = mMaxLayoutHeight - getIntrinsicHeight(); + if (position < maxShelfStart && position + row.getIntrinsicHeight() >= maxShelfStart + && row.getTranslationY() < position) { + iconState.isLastExpandIcon = true; + iconState.customTransformHeight = NO_VALUE; + // Let's check if we're close enough to snap into the shelf + boolean forceInShelf = mMaxLayoutHeight - getIntrinsicHeight() - position + < getIntrinsicHeight(); + if (!forceInShelf) { + // We are overlapping the shelf but not enough, so the icon needs to be + // repositioned + iconState.customTransformHeight = (int) (mMaxLayoutHeight + - getIntrinsicHeight() - position); + } + } + } float fullTransitionAmount; float iconTransitionAmount; float shelfStart = getTranslationY(); + if (iconState.hasCustomTransformHeight()) { + fullHeight = iconState.customTransformHeight; + iconTransformDistance = iconState.customTransformHeight; + } + boolean fullyInOrOut = true; if (viewEnd >= shelfStart && (!mAmbientState.isUnlockHintRunning() || row.isInShelf()) && (mAmbientState.isShadeExpanded() || (!row.isPinned() && !row.isHeadsUpAnimatingAway()))) { if (viewStart < shelfStart) { - float fullAmount = (shelfStart - viewStart) / fullHeight; + fullAmount = Math.min(1.0f, fullAmount); float interpolatedAmount = Interpolators.ACCELERATE_DECELERATE.getInterpolation( fullAmount); interpolatedAmount = NotificationUtils.interpolate( @@ -344,7 +386,7 @@ public class NotificationShelf extends ActivatableNotificationView implements iconTransitionAmount = (shelfStart - viewStart) / iconTransformDistance; iconTransitionAmount = Math.min(1.0f, iconTransitionAmount); iconTransitionAmount = 1.0f - iconTransitionAmount; - + fullyInOrOut = false; } else { fullTransitionAmount = 1.0f; iconTransitionAmount = 1.0f; @@ -353,6 +395,10 @@ public class NotificationShelf extends ActivatableNotificationView implements fullTransitionAmount = 0.0f; iconTransitionAmount = 0.0f; } + if (fullyInOrOut && !expandingAnimated && iconState.isLastExpandIcon) { + iconState.isLastExpandIcon = false; + iconState.customTransformHeight = NO_VALUE; + } updateIconPositioning(row, iconTransitionAmount, fullTransitionAmount, iconTransformDistance, scrolling, scrollingFast, expandingAnimated, isLastChild); return fullTransitionAmount; @@ -366,9 +412,10 @@ public class NotificationShelf extends ActivatableNotificationView implements if (iconState == null) { return; } + boolean forceInShelf = iconState.isLastExpandIcon && !iconState.hasCustomTransformHeight(); float clampedAmount = iconTransitionAmount > 0.5f ? 1.0f : 0.0f; if (clampedAmount == fullTransitionAmount) { - iconState.noAnimations = scrollingFast || expandingAnimated; + iconState.noAnimations = (scrollingFast || expandingAnimated) && !forceInShelf; iconState.useFullTransitionAmount = iconState.noAnimations || (!ICON_ANMATIONS_WHILE_SCROLLING && fullTransitionAmount == 0.0f && scrolling); iconState.useLinearTransitionAmount = !ICON_ANMATIONS_WHILE_SCROLLING @@ -376,12 +423,18 @@ public class NotificationShelf extends ActivatableNotificationView implements iconState.translateContent = mMaxLayoutHeight - getTranslationY() - getIntrinsicHeight() > 0; } - if (scrollingFast || (expandingAnimated && iconState.useFullTransitionAmount - && !ViewState.isAnimatingY(icon))) { + if (!forceInShelf && (scrollingFast || (expandingAnimated + && iconState.useFullTransitionAmount && !ViewState.isAnimatingY(icon)))) { iconState.cancelAnimations(icon); iconState.useFullTransitionAmount = true; iconState.noAnimations = true; } + if (iconState.hasCustomTransformHeight()) { + iconState.useFullTransitionAmount = true; + } + if (iconState.isLastExpandIcon) { + iconState.translateContent = false; + } float transitionAmount; if (isLastChild || !USE_ANIMATIONS_WHEN_OPENING || iconState.useFullTransitionAmount || iconState.useLinearTransitionAmount) { @@ -548,8 +601,7 @@ public class NotificationShelf extends ActivatableNotificationView implements if (!hasOverflow) { // we have to ensure that adding the low priority notification won't lead to an // overflow - collapsedPadding -= (1.0f + NotificationIconContainer.OVERFLOW_EARLY_AMOUNT) - * mCollapsedIcons.getIconSize(); + collapsedPadding -= (1.0f + OVERFLOW_EARLY_AMOUNT) * mCollapsedIcons.getIconSize(); } float padding = NotificationUtils.interpolate(collapsedPadding, mShelfIcons.getPaddingEnd(), diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java index c608a8594f6f..680f693a83f8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java @@ -76,6 +76,7 @@ public class CarStatusBar extends StatusBar implements private Drawable mNotificationPanelBackground; private ConnectedDeviceSignalController mConnectedDeviceSignalController; + private ViewGroup mNavigationBarWindow; private CarNavigationBarView mNavigationBarView; private final Object mQueueLock = new Object(); @@ -97,6 +98,11 @@ public class CarStatusBar extends StatusBar implements mCarBatteryController.stopListening(); mConnectedDeviceSignalController.stopListening(); + if (mNavigationBarWindow != null) { + mWindowManager.removeViewImmediate(mNavigationBarWindow); + mNavigationBarView = null; + } + super.destroy(); } @@ -153,10 +159,19 @@ public class CarStatusBar extends StatusBar implements // SystemUI requires that the navigation bar view have a parent. Since the regular // StatusBar inflates navigation_bar_window as this parent view, use the same view for the // CarNavigationBarView. - ViewGroup navigationBarWindow = (ViewGroup) View.inflate(mContext, + mNavigationBarWindow = (ViewGroup) View.inflate(mContext, R.layout.navigation_bar_window, null); - View.inflate(mContext, R.layout.car_navigation_bar, navigationBarWindow); - mNavigationBarView = (CarNavigationBarView) navigationBarWindow.getChildAt(0); + if (mNavigationBarWindow == null) { + Log.e(TAG, "CarStatusBar failed inflate for R.layout.navigation_bar_window"); + } + + + View.inflate(mContext, R.layout.car_navigation_bar, mNavigationBarWindow); + mNavigationBarView = (CarNavigationBarView) mNavigationBarWindow.getChildAt(0); + if (mNavigationBarView == null) { + Log.e(TAG, "CarStatusBar failed inflate for R.layout.car_navigation_bar"); + } + mController = new CarNavigationBarController(mContext, mNavigationBarView, this /* ActivityStarter*/); @@ -173,7 +188,7 @@ public class CarStatusBar extends StatusBar implements lp.setTitle("CarNavigationBar"); lp.windowAnimations = 0; - mWindowManager.addView(navigationBarWindow, lp); + mWindowManager.addView(mNavigationBarWindow, lp); } @Override @@ -219,6 +234,11 @@ public class CarStatusBar extends StatusBar implements } @Override + public View getNavigationBarWindow() { + return mNavigationBarWindow; + } + + @Override protected View.OnTouchListener getStatusBarWindowTouchListener() { // Usually, a touch on the background window will dismiss the notification shade. However, // for the car use-case, the shade should remain unless the user switches to a different diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java index a14d1bc564ff..09ae521ceb2b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java @@ -37,6 +37,7 @@ import com.android.systemui.R; import com.android.systemui.plugins.PluginListener; import com.android.systemui.plugins.PluginManager; import com.android.systemui.plugins.statusbar.phone.NavBarButtonProvider; +import com.android.systemui.statusbar.phone.ReverseLinearLayout.ReverseFrameLayout; import com.android.systemui.statusbar.policy.KeyButtonView; import com.android.systemui.tuner.TunerService; import com.android.systemui.tuner.TunerService.Tunable; @@ -285,7 +286,7 @@ public class NavigationBarInflaterView extends FrameLayout if (sizeStr.contains(WEIGHT_SUFFIX)) { float weight = Float.parseFloat(sizeStr.substring(0, sizeStr.indexOf(WEIGHT_SUFFIX))); - FrameLayout frame = new FrameLayout(mContext); + FrameLayout frame = new ReverseFrameLayout(mContext); LayoutParams childParams = new LayoutParams(v.getLayoutParams()); if (sizeStr.endsWith(WEIGHT_CENTERED_SUFFIX)) { childParams.gravity = Gravity.CENTER; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java index a6cd472f2686..18dc7e2fa35b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java @@ -575,15 +575,17 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav mRotatedViews[Surface.ROTATION_270] = mRotatedViews[Surface.ROTATION_90] = findViewById(R.id.rot90); - updateCurrentView(); + mCurrentRotation = -1; + reorient(); } public boolean needsReorient(int rotation) { return mCurrentRotation != rotation; } - private void updateCurrentView() { + private boolean updateCurrentView() { final int rot = mDisplay.getRotation(); + if (rot == mCurrentRotation) return false; for (int i=0; i<4; i++) { mRotatedViews[i].setVisibility(View.GONE); } @@ -595,6 +597,7 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav } updateLayoutTransitionsEnabled(); mCurrentRotation = rot; + return true; } private void updateRecentsIcon() { @@ -607,10 +610,15 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav } public void reorient() { - updateCurrentView(); + if (!updateCurrentView()) { + return; + } + Log.d(TAG, "reorient", new Throwable()); mDeadZone = (DeadZone) mCurrentView.findViewById(R.id.deadzone); - ((NavigationBarFrame) getRootView()).setDeadZone(mDeadZone); + if (getRootView() instanceof NavigationBarFrame) { + ((NavigationBarFrame) getRootView()).setDeadZone(mDeadZone); + } mDeadZone.setDisplayRotation(mCurrentRotation); // force the low profile & disabled states into compliance @@ -644,6 +652,7 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav mVertical = newVertical; //Log.v(TAG, String.format("onSizeChanged: h=%d, w=%d, vert=%s", h, w, mVertical?"y":"n")); reorient(); + getHomeButton().setVertical(mVertical); notifyVerticalChangedListener(newVertical); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java index 3937dd3eea2a..38c8d31e0970 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java @@ -526,6 +526,7 @@ public class NotificationIconContainer extends AlphaOptimizedFrameLayout { } public class IconState extends ViewState { + public static final int NO_VALUE = NotificationIconContainer.NO_VALUE; public float iconAppearAmount = 1.0f; public float clampedAppearAmount = 1.0f; public int visibleState; @@ -538,6 +539,8 @@ public class NotificationIconContainer extends AlphaOptimizedFrameLayout { public boolean justUndarkened; public int iconColor = StatusBarIconView.NO_COLOR; public boolean noAnimations; + public boolean isLastExpandIcon; + public int customTransformHeight = NO_VALUE; @Override public void applyToView(View view) { @@ -615,6 +618,10 @@ public class NotificationIconContainer extends AlphaOptimizedFrameLayout { justUndarkened = false; } + public boolean hasCustomTransformHeight() { + return isLastExpandIcon && customTransformHeight != NO_VALUE; + } + @Override public void initFrom(View view) { super.initFrom(view); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java index 4701f85c5ff3..a7731724b807 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java @@ -153,7 +153,6 @@ public class NotificationPanelView extends PanelView implements protected int mQsMinExpansionHeight; protected int mQsMaxExpansionHeight; private int mQsPeekHeight; - private boolean mQsOverscrollExpansionEnabled; private boolean mStackScrollerOverscrolling; private boolean mQsExpansionFromOverscroll; private float mLastOverscroll; @@ -242,8 +241,6 @@ public class NotificationPanelView extends PanelView implements super(context, attrs); setWillNotDraw(!DEBUG); mFalsingManager = FalsingManager.getInstance(context); - mQsOverscrollExpansionEnabled = - getResources().getBoolean(R.bool.config_enableQuickSettingsOverscrollExpansion); } public void setStatusBar(StatusBar bar) { @@ -668,7 +665,7 @@ public class NotificationPanelView extends PanelView implements return true; } - if (mQsOverscrollExpansionEnabled && !isFullyCollapsed() && onQsIntercept(event)) { + if (!isFullyCollapsed() && onQsIntercept(event)) { return true; } return super.onInterceptTouchEvent(event); @@ -843,8 +840,7 @@ public class NotificationPanelView extends PanelView implements } handled |= mHeadsUpTouchHelper.onTouchEvent(event); - if (mQsOverscrollExpansionEnabled && !mHeadsUpTouchHelper.isTrackingHeadsUp() - && handleQsTouch(event)) { + if (!mHeadsUpTouchHelper.isTrackingHeadsUp() && handleQsTouch(event)) { return true; } if (event.getActionMasked() == MotionEvent.ACTION_DOWN && isFullyCollapsed()) { @@ -1028,10 +1024,6 @@ public class NotificationPanelView extends PanelView implements @Override public void onOverscrollTopChanged(float amount, boolean isRubberbanded) { - if (!mQsOverscrollExpansionEnabled) { - return; - } - cancelQsAnimation(); if (!mQsExpansionEnabled) { amount = 0f; @@ -1046,10 +1038,6 @@ public class NotificationPanelView extends PanelView implements @Override public void flingTopOverscroll(float velocity, boolean open) { - if (!mQsOverscrollExpansionEnabled) { - return; - } - mLastOverscroll = 0f; mQsExpansionFromOverscroll = false; setQsExpansion(mQsExpansionHeight); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ReverseLinearLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ReverseLinearLayout.java index f45967a0a0a6..bcbc3457a2e2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ReverseLinearLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ReverseLinearLayout.java @@ -16,10 +16,10 @@ package com.android.systemui.statusbar.phone; import android.annotation.Nullable; import android.content.Context; -import android.content.res.Configuration; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; +import android.widget.FrameLayout; import android.widget.LinearLayout; import java.util.ArrayList; @@ -48,7 +48,7 @@ public class ReverseLinearLayout extends LinearLayout { @Override public void addView(View child) { - reversParams(child.getLayoutParams()); + reverseParams(child.getLayoutParams(), child); if (mIsLayoutReverse) { super.addView(child, 0); } else { @@ -58,7 +58,7 @@ public class ReverseLinearLayout extends LinearLayout { @Override public void addView(View child, ViewGroup.LayoutParams params) { - reversParams(params); + reverseParams(params, child); if (mIsLayoutReverse) { super.addView(child, 0, params); } else { @@ -100,7 +100,15 @@ public class ReverseLinearLayout extends LinearLayout { } } - private void reversParams(ViewGroup.LayoutParams params) { + private static void reverseParams(ViewGroup.LayoutParams params, View child) { + if (child instanceof Reversable) { + ((Reversable) child).reverse(); + } + if (child.getPaddingLeft() == child.getPaddingRight() + && child.getPaddingTop() == child.getPaddingBottom()) { + child.setPadding(child.getPaddingTop(), child.getPaddingLeft(), + child.getPaddingTop(), child.getPaddingLeft()); + } if (params == null) { return; } @@ -109,4 +117,23 @@ public class ReverseLinearLayout extends LinearLayout { params.height = width; } + public interface Reversable { + void reverse(); + } + + public static class ReverseFrameLayout extends FrameLayout implements Reversable { + + public ReverseFrameLayout(Context context) { + super(context); + } + + @Override + public void reverse() { + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + reverseParams(child.getLayoutParams(), child); + } + } + } + } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SignalDrawable.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SignalDrawable.java index deea521b7c2f..7a7efbdc6615 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SignalDrawable.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SignalDrawable.java @@ -100,12 +100,8 @@ public class SignalDrawable extends Drawable { // How far the circle defining the corners is inset from the edges private final float mAppliedCornerInset; - // The easiest way to understand this is as if we set Style.STROKE and draw the triangle, - // but that is only theoretically right. Instead, draw the triangle and clip out a smaller - // one inset by this amount. - private final float mEmptyStrokeWidth; private static final float INV_TAN = 1f / (float) Math.tan(Math.PI / 8f); - private final float mEmptyDiagInset; // == mEmptyStrokeWidth * INV_TAN + private static final float CUT_WIDTH_DP = 1f / 12f; // Where the top and left points of the triangle would be if not for rounding private final PointF mVirtualTop = new PointF(); @@ -145,11 +141,6 @@ public class SignalDrawable extends Drawable { Utils.getDefaultColor(context, R.color.light_mode_icon_color_dual_tone_fill); mIntrinsicSize = context.getResources().getDimensionPixelSize(R.dimen.signal_icon_size); - // mCutPath parameters - mEmptyStrokeWidth = context.getResources() - .getDimensionPixelSize(R.dimen.mobile_signal_empty_strokewidth); - mEmptyDiagInset = mEmptyStrokeWidth * INV_TAN; - mHandler = new Handler(); setDarkIntensity(0); @@ -326,22 +317,20 @@ public class SignalDrawable extends Drawable { (padding + cornerRadius + mAppliedCornerInset) - (INV_TAN * cornerRadius), height - padding); + final float cutWidth = CUT_WIDTH_DP * height; + final float cutDiagInset = cutWidth * INV_TAN; + // Cut out a smaller triangle from the center of mFullPath mCutPath.reset(); mCutPath.setFillType(FillType.WINDING); - mCutPath.moveTo(width - padding - mEmptyStrokeWidth, - height - padding - mEmptyStrokeWidth); - mCutPath.lineTo(width - padding - mEmptyStrokeWidth, - mVirtualTop.y + mEmptyDiagInset); - mCutPath.lineTo(mVirtualLeft.x + mEmptyDiagInset, - height - padding - mEmptyStrokeWidth); - mCutPath.lineTo(width - padding - mEmptyStrokeWidth, - height - padding - mEmptyStrokeWidth); - - // In empty state, draw the full path as the foreground paint - mForegroundPath.set(mFullPath); - mFullPath.reset(); - mForegroundPath.op(mCutPath, Path.Op.DIFFERENCE); + mCutPath.moveTo(width - padding - cutWidth, height - padding - cutWidth); + mCutPath.lineTo(width - padding - cutWidth, mVirtualTop.y + cutDiagInset); + mCutPath.lineTo(mVirtualLeft.x + cutDiagInset, height - padding - cutWidth); + mCutPath.lineTo(width - padding - cutWidth, height - padding - cutWidth); + + // Draw empty state as only background + mForegroundPath.reset(); + mFullPath.op(mCutPath, Path.Op.DIFFERENCE); } else if (mState == STATE_AIRPLANE) { // Airplane mode is slashed, full-signal mForegroundPath.set(mFullPath); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java index 4d6fd9c136b5..714c287365b3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java @@ -182,6 +182,7 @@ import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.Snoo import com.android.systemui.qs.QSFragment; import com.android.systemui.qs.QSPanel; import com.android.systemui.qs.QSTileHost; +import com.android.systemui.qs.car.CarQSFragment; import com.android.systemui.recents.Recents; import com.android.systemui.recents.ScreenPinningRequest; import com.android.systemui.recents.events.EventBus; @@ -1149,7 +1150,7 @@ public class StatusBar extends SystemUI implements DemoMode, ExtensionFragmentListener.attachExtensonToFragment(container, QS.TAG, R.id.qs_frame, Dependency.get(ExtensionController.class).newExtension(QS.class) .withPlugin(QS.class) - .withUiMode(UI_MODE_TYPE_CAR, () -> new QSFragment()) + .withUiMode(UI_MODE_TYPE_CAR, () -> new CarQSFragment()) .withDefault(() -> new QSFragment()) .build()); final QSTileHost qsh = SystemUIFactory.getInstance().createQSTileHost(mContext, this, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ExtensionControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ExtensionControllerImpl.java index c2618cd5edaf..b79137ea68ff 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ExtensionControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ExtensionControllerImpl.java @@ -262,7 +262,8 @@ public class ExtensionControllerImpl implements ExtensionController { public UiModeItem(int uiMode, Supplier<T> supplier) { mDesiredUiMode = uiMode; mSupplier = supplier; - mUiMode = mDefaultContext.getResources().getConfiguration().uiMode; + mUiMode = mDefaultContext.getResources().getConfiguration().uiMode + & Configuration.UI_MODE_TYPE_MASK; Dependency.get(ConfigurationController.class).addCallback(this); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/AmbientState.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/AmbientState.java index b7b991e36710..ba1e7c2d86c5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/AmbientState.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/AmbientState.java @@ -62,6 +62,7 @@ public class AmbientState { private boolean mHasPulsingNotifications; private boolean mUnlockHintRunning; private boolean mQsCustomizerShowing; + private int mIntrinsicPadding; public AmbientState(Context context) { reload(context); @@ -323,4 +324,12 @@ public class AmbientState { public void setQsCustomizerShowing(boolean qsCustomizerShowing) { mQsCustomizerShowing = qsCustomizerShowing; } + + public void setIntrinsicPadding(int intrinsicPadding) { + mIntrinsicPadding = intrinsicPadding; + } + + public int getIntrinsicPadding() { + return mIntrinsicPadding; + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java index cbd315b940f3..41cde9c7dd5e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java @@ -2735,7 +2735,7 @@ public class NotificationStackScrollLayout extends ViewGroup return view.getHeight(); } - private int getPositionInLinearLayout(View requestedView) { + public int getPositionInLinearLayout(View requestedView) { ExpandableNotificationRow childInGroup = null; ExpandableNotificationRow requestedRow = null; if (isChildInGroup(requestedView)) { @@ -3650,6 +3650,7 @@ public class NotificationStackScrollLayout extends ViewGroup public void setIntrinsicPadding(int intrinsicPadding) { mIntrinsicPadding = intrinsicPadding; + mAmbientState.setIntrinsicPadding(intrinsicPadding); } public int getIntrinsicPadding() { diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java index 5d51a33c6307..eaad2f9bd457 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java @@ -43,6 +43,7 @@ import android.provider.Settings; import android.service.notification.Condition; import android.util.ArrayMap; import android.util.Log; +import android.view.accessibility.AccessibilityManager; import com.android.internal.annotations.GuardedBy; import com.android.systemui.Dumpable; @@ -122,6 +123,12 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa mReceiver.init(); mVibrator = (Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE); mHasVibrator = mVibrator != null && mVibrator.hasVibrator(); + + boolean accessibilityVolumeStreamActive = context.getSystemService( + AccessibilityManager.class).isAccessibilityVolumeStreamActive(); + mVolumeController.setA11yMode(accessibilityVolumeStreamActive ? + VolumePolicy.A11Y_MODE_INDEPENDENT_A11Y_VOLUME : + VolumePolicy.A11Y_MODE_MEDIA_A11Y_VOLUME); } public AudioManager getAudioManager() { @@ -210,6 +217,7 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa public void addCallback(Callbacks callback, Handler handler) { mCallbacks.add(callback, handler); + callback.onAccessibilityModeChanged(mShowA11yStream); } public void setUserActivityListener(UserActivityListener listener) { diff --git a/packages/SystemUI/tests/AndroidManifest.xml b/packages/SystemUI/tests/AndroidManifest.xml index b12fd1c9ba89..9bb21808e9bc 100644 --- a/packages/SystemUI/tests/AndroidManifest.xml +++ b/packages/SystemUI/tests/AndroidManifest.xml @@ -48,7 +48,7 @@ android:process=":killable" /> </application> - <instrumentation android:name="android.support.test.runner.AndroidJUnitRunner" + <instrumentation android:name="android.testing.TestableInstrumentation" android:targetPackage="com.android.systemui.tests" android:label="Tests for SystemUI"> </instrumentation> diff --git a/packages/SystemUI/tests/src/com/android/systemui/RoundedCornersTest.java b/packages/SystemUI/tests/src/com/android/systemui/RoundedCornersTest.java index 945af34c1df1..13ed2a25abf9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/RoundedCornersTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/RoundedCornersTest.java @@ -21,7 +21,6 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; @@ -30,7 +29,6 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.Fragment; -import android.support.test.filters.FlakyTest; import android.support.test.filters.SmallTest; import android.testing.AndroidTestingRunner; import android.view.Display; @@ -53,7 +51,6 @@ import org.junit.Test; import org.junit.runner.RunWith; @RunWith(AndroidTestingRunner.class) -@FlakyTest @SmallTest public class RoundedCornersTest extends SysuiTestCase { @@ -72,6 +69,7 @@ public class RoundedCornersTest extends SysuiTestCase { mWindowManager = mock(WindowManager.class); mView = spy(new StatusBarWindowView(mContext, null)); when(mStatusBar.getStatusBarWindow()).thenReturn(mView); + when(mStatusBar.getNavigationBarWindow()).thenReturn(mView); mContext.putComponent(StatusBar.class, mStatusBar); Display display = mContext.getSystemService(WindowManager.class).getDefaultDisplay(); @@ -94,6 +92,8 @@ public class RoundedCornersTest extends SysuiTestCase { @Test public void testNoRounding() { mContext.getOrCreateTestableResources().addOverride(dimen.rounded_corner_radius, 0); + mContext.getOrCreateTestableResources() + .addOverride(dimen.rounded_corner_content_padding, 0); mRoundedCorners.start(); // No views added. @@ -105,8 +105,11 @@ public class RoundedCornersTest extends SysuiTestCase { } @Test + @Ignore public void testRounding() { mContext.getOrCreateTestableResources().addOverride(dimen.rounded_corner_radius, 20); + mContext.getOrCreateTestableResources() + .addOverride(dimen.rounded_corner_content_padding, 20); mRoundedCorners.start(); // Add 2 windows for rounded corners (top and bottom). diff --git a/packages/SystemUI/tests/src/com/android/systemui/SysuiTestCase.java b/packages/SystemUI/tests/src/com/android/systemui/SysuiTestCase.java index 361a20fb7bdc..66d00dd6c5c6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/SysuiTestCase.java +++ b/packages/SystemUI/tests/src/com/android/systemui/SysuiTestCase.java @@ -31,6 +31,8 @@ import android.util.Log; import org.junit.After; import org.junit.Before; import org.junit.Rule; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; @@ -56,10 +58,14 @@ public abstract class SysuiTestCase { mRealInstrumentation = InstrumentationRegistry.getInstrumentation(); Instrumentation inst = spy(mRealInstrumentation); - when(inst.getContext()).thenThrow(new RuntimeException( - "SysUI Tests should use SysuiTestCase#getContext or SysuiTestCase#mContext")); - when(inst.getTargetContext()).thenThrow(new RuntimeException( - "SysUI Tests should use SysuiTestCase#getContext or SysuiTestCase#mContext")); + when(inst.getContext()).thenAnswer(invocation -> { + throw new RuntimeException( + "SysUI Tests should use SysuiTestCase#getContext or SysuiTestCase#mContext"); + }); + when(inst.getTargetContext()).thenAnswer(invocation -> { + throw new RuntimeException( + "SysUI Tests should use SysuiTestCase#getContext or SysuiTestCase#mContext"); + }); InstrumentationRegistry.registerInstance(inst, InstrumentationRegistry.getArguments()); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java index 2363b2aadc10..8641faca5d6e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java @@ -27,7 +27,6 @@ import android.app.Instrumentation; import android.os.Handler; import android.os.Looper; import android.support.test.InstrumentationRegistry; -import android.support.test.filters.FlakyTest; import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; diff --git a/packages/SystemUI/tests/src/com/android/systemui/plugins/PluginManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/plugins/PluginManagerTest.java index b8e9fcd29096..bba982c3b060 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/plugins/PluginManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/plugins/PluginManagerTest.java @@ -26,8 +26,6 @@ import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; -import android.support.test.annotation.UiThreadTest; -import android.support.test.runner.AndroidJUnit4; import android.test.suitebuilder.annotation.SmallTest; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; @@ -36,11 +34,10 @@ import android.testing.TestableLooper.RunWithLooper; import com.android.internal.messages.nano.SystemMessageProto.SystemMessage; import com.android.systemui.Dependency; import com.android.systemui.SysuiTestCase; -import com.android.systemui.plugins.annotations.ProvidesInterface; import com.android.systemui.plugins.PluginInstanceManager.PluginInfo; +import com.android.systemui.plugins.annotations.ProvidesInterface; import com.android.systemui.plugins.PluginManagerImpl.PluginInstanceManagerFactory; -import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSDetailTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSDetailTest.java index 6a85511daca2..ed47fbb89077 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSDetailTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSDetailTest.java @@ -23,10 +23,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import org.junit.After; -import org.junit.Ignore; import android.support.test.filters.SmallTest; -import android.support.test.filters.FlakyTest; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.testing.TestableLooper.RunWithLooper; @@ -41,6 +38,7 @@ import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.qs.DetailAdapter; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFooterTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFooterImplTest.java index f4fda065860e..703b4d5e22ec 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFooterTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFooterImplTest.java @@ -21,7 +21,6 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.support.test.filters.SmallTest; -import android.support.test.filters.FlakyTest; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.testing.TestableLooper.RunWithLooper; @@ -43,9 +42,9 @@ import org.junit.runner.RunWith; @RunWithLooper @SmallTest @Ignore("failing") -public class QSFooterTest extends LeakCheckedTest { +public class QSFooterImplTest extends LeakCheckedTest { - private QSFooter mFooter; + private QSFooterImpl mFooter; private ActivityStarter mActivityStarter; private DeviceProvisionedController mDeviceProvisionedController; @@ -55,9 +54,9 @@ public class QSFooterTest extends LeakCheckedTest { mActivityStarter = mDependency.injectMockDependency(ActivityStarter.class); mDeviceProvisionedController = mDependency.injectMockDependency( DeviceProvisionedController.class); - TestableLooper.get(this).runWithLooper(() -> { - mFooter = (QSFooter) LayoutInflater.from(mContext).inflate(R.layout.qs_footer, null); - }); + TestableLooper.get(this).runWithLooper( + () -> mFooter = (QSFooterImpl) LayoutInflater.from(mContext).inflate( + R.layout.qs_footer_impl, null)); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.java index 637b2440af09..85cdfcc2ce09 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.java @@ -20,7 +20,6 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.support.test.filters.SmallTest; -import android.support.test.filters.FlakyTest; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.testing.TestableLooper.RunWithLooper; @@ -31,7 +30,6 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.qs.customize.QSCustomizer; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; @@ -40,7 +38,6 @@ import java.util.Collections; @RunWith(AndroidTestingRunner.class) @RunWithLooper @SmallTest -@FlakyTest public class QSPanelTest extends SysuiTestCase { private MetricsLogger mMetricsLogger; diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/car/CarQsFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/car/CarQsFragmentTest.java new file mode 100644 index 000000000000..4f87b02ed35f --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/car/CarQsFragmentTest.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package com.android.systemui.qs.car; + +import static org.junit.Assert.assertNotNull; + +import android.content.Context; +import android.support.test.filters.SmallTest; +import android.testing.AndroidTestingRunner; +import android.testing.LayoutInflaterBuilder; +import android.testing.TestableLooper; +import android.testing.TestableLooper.RunWithLooper; +import android.view.View; +import android.widget.FrameLayout; + +import com.android.keyguard.CarrierText; +import com.android.systemui.Dependency; +import com.android.systemui.SysuiBaseFragmentTest; +import com.android.systemui.statusbar.policy.Clock; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Tests for {@link CarQSFragment}. + */ +@RunWith(AndroidTestingRunner.class) +@RunWithLooper(setAsMainLooper = true) +@SmallTest +public class CarQsFragmentTest extends SysuiBaseFragmentTest { + public CarQsFragmentTest() { + super(CarQSFragment.class); + } + + @Before + public void initDependencies() { + mContext.addMockSystemService(Context.LAYOUT_INFLATER_SERVICE, + new LayoutInflaterBuilder(mContext) + .replace("com.android.systemui.statusbar.policy.SplitClockView", + FrameLayout.class) + .replace("TextClock", View.class) + .replace(CarrierText.class, View.class) + .replace(Clock.class, View.class) + .build()); + + mDependency.injectTestDependency(Dependency.BG_LOOPER, + TestableLooper.get(this).getLooper()); + } + + @Test + public void testLayoutInflation() { + CarQSFragment fragment = (CarQSFragment) mFragment; + mFragments.dispatchResume(); + + assertNotNull(fragment.getHeader()); + assertNotNull(fragment.getFooter()); + } + + @Test + public void testListening() { + CarQSFragment qs = (CarQSFragment) mFragment; + mFragments.dispatchResume(); + processAllMessages(); + + qs.setListening(true); + processAllMessages(); + + qs.setListening(false); + processAllMessages(); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/ExpandableNotificationRowTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ExpandableNotificationRowTest.java index 664ea710d61e..2f6511c8481e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/ExpandableNotificationRowTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ExpandableNotificationRowTest.java @@ -21,27 +21,21 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; -import android.content.Context; -import android.support.test.InstrumentationRegistry; -import android.support.test.annotation.UiThreadTest; -import android.support.test.filters.FlakyTest; import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; import android.view.View; +import com.android.systemui.SysuiTestCase; import com.android.systemui.statusbar.notification.AboveShelfChangedListener; import com.android.systemui.statusbar.stack.NotificationChildrenContainer; -import com.android.systemui.SysuiTestCase; import org.junit.Assert; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; @SmallTest @RunWith(AndroidJUnit4.class) -@FlakyTest public class ExpandableNotificationRowTest extends SysuiTestCase { private ExpandableNotificationRow mGroup; diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationContentViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationContentViewTest.java index 4dce2f5f87ed..436849c9d700 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationContentViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationContentViewTest.java @@ -16,30 +16,25 @@ package com.android.systemui.statusbar; -import android.content.Context; -import android.support.test.InstrumentationRegistry; +import static org.mockito.ArgumentMatchers.anyFloat; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; + import android.support.test.annotation.UiThreadTest; -import android.support.test.filters.FlakyTest; import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; import android.view.View; +import com.android.systemui.SysuiTestCase; + import org.junit.Assert; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; -import static org.mockito.ArgumentMatchers.anyFloat; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.spy; - -import com.android.systemui.SysuiTestCase; - @SmallTest @RunWith(AndroidJUnit4.class) -@FlakyTest public class NotificationContentViewTest extends SysuiTestCase { NotificationContentView mView; diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationCustomViewWrapperTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationCustomViewWrapperTest.java index d18e63bcc151..6e59d10aad3c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationCustomViewWrapperTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationCustomViewWrapperTest.java @@ -16,10 +16,7 @@ package com.android.systemui.statusbar; -import android.content.Context; -import android.support.test.InstrumentationRegistry; import android.support.test.annotation.UiThreadTest; -import android.support.test.filters.FlakyTest; import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; import android.view.View; @@ -32,13 +29,11 @@ import com.android.systemui.statusbar.notification.NotificationViewWrapper; import org.junit.Assert; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; @SmallTest @RunWith(AndroidJUnit4.class) -@FlakyTest public class NotificationCustomViewWrapperTest extends SysuiTestCase { private ExpandableNotificationRow mRow; diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/stack/NotificationChildrenContainerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/stack/NotificationChildrenContainerTest.java index bc6833df1e30..5ac965cf4042 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/stack/NotificationChildrenContainerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/stack/NotificationChildrenContainerTest.java @@ -16,10 +16,6 @@ package com.android.systemui.statusbar.stack; -import android.content.Context; -import android.support.test.InstrumentationRegistry; -import android.support.test.annotation.UiThreadTest; -import android.support.test.filters.FlakyTest; import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; import android.view.NotificationHeaderView; @@ -31,13 +27,11 @@ import com.android.systemui.statusbar.NotificationTestHelper; import org.junit.Assert; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; @SmallTest @RunWith(AndroidJUnit4.class) -@FlakyTest public class NotificationChildrenContainerTest extends SysuiTestCase { private ExpandableNotificationRow mGroup; diff --git a/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakePluginManager.java b/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakePluginManager.java index 0a83a896dfaf..d1b1c5b9a066 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakePluginManager.java +++ b/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakePluginManager.java @@ -14,7 +14,6 @@ package com.android.systemui.utils.leaks; -import android.content.Context; import android.testing.LeakCheck; import com.android.systemui.plugins.Plugin; diff --git a/proto/src/metrics_constants.proto b/proto/src/metrics_constants.proto index 39ee6cf4a234..c6e3ed5f30ae 100644 --- a/proto/src/metrics_constants.proto +++ b/proto/src/metrics_constants.proto @@ -4153,11 +4153,42 @@ message MetricsEvent { // OS: O DR FIELD_PLUG_BATTERY_PERCENTAGE = 1024; + // Device Headset battery level on Plug + // CATEGORY: OTHER + // FIELD - The battery percentage when the user decided to plug in + // Type: integer + // OS: O DR + FIELD_UNPLUG_BATTERY_PERCENTAGE = 1025; + // Device Headset Pose status // CATEGORY: OTHER // SUBTYPE: 1 is 6DOF, 2 is 3DOF // OS: O DR - ACTION_HEADSET_POSE_STATUS = 1025; + ACTION_HEADSET_POSE_STATUS = 1026; + + // Device Headset Usage session time + // CATEGORY: OTHER + // FIELD - The time the headset was used in a session + // OS: O DR + FIELD_SESSION_TIME_MS = 1027; + + // Device Headset Idle time + // CATEGORY: OTHER + // FIELD - The time in between each session + // OS: O DR + FIELD_TIME_ELAPSED_BETWEEN_SESSION_MS = 1028; + + // Device Headset charge session time + // CATEGORY: OTHER + // FIELD - The time taken for each charge + // OS: O DR + FIELD_TIME_OF_CHARGE_MS = 1029; + + // Device Headset time between charge + // CATEGORY: OTHER + // FIELD - The time in between each charge + // OS: O DR + FIELD_TIME_ELAPSED_BETWEEN_CHARGE_MS = 1030; // Add new aosp constants above this line. // END OF AOSP CONSTANTS diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java index 9e4d89cbc9c5..0e42e6d6a83d 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java @@ -271,6 +271,7 @@ class AccessibilityInputFilter extends InputFilter implements EventStreamTransfo private void processKeyEvent(EventStreamState state, KeyEvent event, int policyFlags) { if (!state.shouldProcessKeyEvent(event)) { + super.onInputEvent(event, policyFlags); return; } mEventHandler.onKeyEvent(event, policyFlags); diff --git a/services/autofill/java/com/android/server/autofill/RemoteFillService.java b/services/autofill/java/com/android/server/autofill/RemoteFillService.java index aebe92e1687d..5e25dfa49d70 100644 --- a/services/autofill/java/com/android/server/autofill/RemoteFillService.java +++ b/services/autofill/java/com/android/server/autofill/RemoteFillService.java @@ -430,6 +430,8 @@ final class RemoteFillService implements DeathRecipient { Slog.w(LOG_TAG, getClass().getSimpleName() + " timed out"); final RemoteFillService remoteService = mWeakService.get(); if (remoteService != null) { + Slog.w(LOG_TAG, getClass().getSimpleName() + " timed out after " + + TIMEOUT_REMOTE_REQUEST_MILLIS + " ms"); fail(remoteService); } }; diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java index 72ad752caf19..25aa0d15b617 100644 --- a/services/autofill/java/com/android/server/autofill/Session.java +++ b/services/autofill/java/com/android/server/autofill/Session.java @@ -55,8 +55,10 @@ import android.service.autofill.Dataset; import android.service.autofill.FillContext; import android.service.autofill.FillRequest; import android.service.autofill.FillResponse; +import android.service.autofill.InternalValidator; import android.service.autofill.SaveInfo; import android.service.autofill.SaveRequest; +import android.service.autofill.ValueFinder; import android.util.ArrayMap; import android.util.ArraySet; import android.util.Slog; @@ -78,6 +80,7 @@ import com.android.server.autofill.ui.AutoFillUI; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; @@ -214,7 +217,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState final int numContexts = mContexts.size(); for (int i = 0; i < numContexts; i++) { - fillContextWithAllowedValues(mContexts.get(i), flags); + fillContextWithAllowedValuesLocked(mContexts.get(i), flags); } request = new FillRequest(requestId, mContexts, mClientState, flags); @@ -227,7 +230,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState /** * Returns the ids of all entries in {@link #mViewStates} in the same order. */ - private AutofillId[] getIdsOfAllViewStates() { + private AutofillId[] getIdsOfAllViewStatesLocked() { final int numViewState = mViewStates.size(); final AutofillId[] ids = new AutofillId[numViewState]; for (int i = 0; i < numViewState; i++) { @@ -238,6 +241,32 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } /** + * Gets the value of a field, using either the {@code viewStates} or the {@code mContexts}, or + * {@code null} when not found on either of them. + */ + @Nullable + private String getValueAsString(@NonNull AutofillId id) { + AutofillValue value = null; + synchronized (mLock) { + final ViewState state = mViewStates.get(id); + if (state == null) { + if (sDebug) Slog.d(TAG, "getValue(): no view state for " + id); + return null; + } + value = state.getCurrentValue(); + if (value == null) { + if (sDebug) Slog.d(TAG, "getValue(): no current value for " + id); + value = getValueFromContexts(id); + } + } + // TODO(b/62534917): support list values, using the String provided by getAutofillOptions() + if (value != null && value.isText()) { + return value.getTextValue().toString(); + } + return null; + } + + /** * Updates values of the nodes in the context's structure so that: * - proper node is focused * - autofillValue is sent back to service when it was previously autofilled @@ -246,8 +275,9 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState * @param fillContext The context to be filled * @param flags The flags that started the session */ - private void fillContextWithAllowedValues(@NonNull FillContext fillContext, int flags) { - final ViewNode[] nodes = fillContext.findViewNodesByAutofillIds(getIdsOfAllViewStates()); + private void fillContextWithAllowedValuesLocked(@NonNull FillContext fillContext, int flags) { + final ViewNode[] nodes = fillContext + .findViewNodesByAutofillIds(getIdsOfAllViewStatesLocked()); final int numViewState = mViewStates.size(); for (int i = 0; i < numViewState; i++) { @@ -771,6 +801,10 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState boolean atLeastOneChanged = false; for (int i = 0; i < requiredIds.length; i++) { final AutofillId id = requiredIds[i]; + if (id == null) { + Slog.w(TAG, "null autofill id on " + Arrays.toString(requiredIds)); + continue; + } final ViewState viewState = mViewStates.get(id); if (viewState == null) { Slog.w(TAG, "showSaveLocked(): no ViewState for required " + id); @@ -832,11 +866,22 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } } if (atLeastOneChanged) { - if (sDebug) Slog.d(TAG, "at least one field changed - showing save UI"); - mService.setSaveShown(id); - getUiForShowing().showSaveUi(mService.getServiceLabel(), saveInfo, mPackageName, - this); + if (sDebug) { + Slog.d(TAG, "at least one field changed, validate fields for save UI"); + } + final ValueFinder valueFinder = (id) -> {return getValueAsString(id);}; + final InternalValidator validator = saveInfo.getValidator(); + if (validator != null && !validator.isValid(valueFinder)) { + // TODO(b/62534917): add CTS test + Slog.i(TAG, "not showing save UI because fields failed validation"); + return true; + } + + if (sDebug) Slog.d(TAG, "Good news, everyone! All checks passed, show save UI!"); + mService.setSaveShown(id); + getUiForShowing().showSaveUi(mService.getServiceLabel(), saveInfo, + valueFinder, mPackageName, this); mIsSaving = true; return false; } @@ -897,7 +942,8 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState for (int contextNum = 0; contextNum < numContexts; contextNum++) { final FillContext context = mContexts.get(contextNum); - final ViewNode[] nodes = context.findViewNodesByAutofillIds(getIdsOfAllViewStates()); + final ViewNode[] nodes = + context.findViewNodesByAutofillIds(getIdsOfAllViewStatesLocked()); if (sVerbose) Slog.v(TAG, "callSaveLocked(): updating " + context); diff --git a/services/autofill/java/com/android/server/autofill/ui/AutoFillUI.java b/services/autofill/java/com/android/server/autofill/ui/AutoFillUI.java index 4f90019d32b6..8b15d506dab7 100644 --- a/services/autofill/java/com/android/server/autofill/ui/AutoFillUI.java +++ b/services/autofill/java/com/android/server/autofill/ui/AutoFillUI.java @@ -28,6 +28,7 @@ import android.os.Handler; import android.service.autofill.Dataset; import android.service.autofill.FillResponse; import android.service.autofill.SaveInfo; +import android.service.autofill.ValueFinder; import android.text.TextUtils; import android.util.Slog; import android.view.autofill.AutofillId; @@ -242,7 +243,8 @@ public final class AutoFillUI { * Shows the UI asking the user to save for autofill. */ public void showSaveUi(@NonNull CharSequence providerLabel, @NonNull SaveInfo info, - @NonNull String packageName, @NonNull AutoFillUiCallback callback) { + @NonNull ValueFinder valueFinder, @NonNull String packageName, + @NonNull AutoFillUiCallback callback) { if (sVerbose) Slog.v(TAG, "showSaveUi() for " + packageName + ": " + info); int numIds = 0; numIds += info.getRequiredIds() == null ? 0 : info.getRequiredIds().length; @@ -257,8 +259,8 @@ public final class AutoFillUI { return; } hideAllUiThread(callback); - mSaveUi = new SaveUi(mContext, providerLabel, info, - mOverlayControl, new SaveUi.OnSaveListener() { + mSaveUi = new SaveUi(mContext, providerLabel, info, valueFinder, mOverlayControl, + new SaveUi.OnSaveListener() { @Override public void onSave() { log.setType(MetricsProto.MetricsEvent.TYPE_ACTION); diff --git a/services/autofill/java/com/android/server/autofill/ui/OverlayControl.java b/services/autofill/java/com/android/server/autofill/ui/OverlayControl.java index fe0611eef0a2..49f4b538448e 100644 --- a/services/autofill/java/com/android/server/autofill/ui/OverlayControl.java +++ b/services/autofill/java/com/android/server/autofill/ui/OverlayControl.java @@ -21,6 +21,7 @@ import android.app.AppOpsManager; import android.content.Context; import android.os.Binder; import android.os.IBinder; +import android.os.UserHandle; /** * This class controls showing/hiding overlays. We don't @@ -47,10 +48,10 @@ class OverlayControl { private void setOverlayAllowed(boolean allowed) { if (mAppOpsManager != null) { - mAppOpsManager.setUserRestriction( - AppOpsManager.OP_SYSTEM_ALERT_WINDOW, !allowed, mToken); - mAppOpsManager.setUserRestriction( - AppOpsManager.OP_TOAST_WINDOW, !allowed, mToken); + mAppOpsManager.setUserRestrictionForUser(AppOpsManager.OP_SYSTEM_ALERT_WINDOW, !allowed, + mToken, null, UserHandle.USER_ALL); + mAppOpsManager.setUserRestrictionForUser(AppOpsManager.OP_TOAST_WINDOW, !allowed, + mToken, null, UserHandle.USER_ALL); } } } diff --git a/services/autofill/java/com/android/server/autofill/ui/SaveUi.java b/services/autofill/java/com/android/server/autofill/ui/SaveUi.java index c9e2a928dee0..e8dc3c1aea06 100644 --- a/services/autofill/java/com/android/server/autofill/ui/SaveUi.java +++ b/services/autofill/java/com/android/server/autofill/ui/SaveUi.java @@ -24,13 +24,18 @@ import android.app.Dialog; import android.content.Context; import android.content.IntentSender; import android.os.Handler; +import android.service.autofill.CustomDescription; import android.service.autofill.SaveInfo; +import android.service.autofill.ValueFinder; import android.text.Html; +import android.text.method.LinkMovementMethod; import android.util.ArraySet; import android.util.Slog; import android.view.Gravity; import android.view.Window; import android.view.WindowManager; +import android.widget.LinearLayout; +import android.widget.RemoteViews; import android.widget.TextView; import android.view.LayoutInflater; import android.view.View; @@ -107,14 +112,15 @@ final class SaveUi { private boolean mDestroyed; SaveUi(@NonNull Context context, @NonNull CharSequence providerLabel, @NonNull SaveInfo info, - @NonNull OverlayControl overlayControl, @NonNull OnSaveListener listener) { + @NonNull ValueFinder valueFinder, @NonNull OverlayControl overlayControl, + @NonNull OnSaveListener listener) { mListener = new OneTimeListener(listener); mOverlayControl = overlayControl; final LayoutInflater inflater = LayoutInflater.from(context); final View view = inflater.inflate(R.layout.autofill_save, null); - final TextView titleView = (TextView) view.findViewById(R.id.autofill_save_title); + final TextView titleView = view.findViewById(R.id.autofill_save_title); final ArraySet<String> types = new ArraySet<>(3); final int type = info.getType(); @@ -135,6 +141,23 @@ final class SaveUi { types.add(context.getString(R.string.autofill_save_type_email_address)); } + final CustomDescription customDescription = info.getCustomDescription(); + + if (customDescription != null) { + // TODO(b/62534917): add CTS test + if (sDebug) Slog.d(TAG, "Using custom description"); + + final RemoteViews presentation = customDescription.getPresentation(valueFinder); + if (presentation != null) { + final View remote = presentation.apply(context, null); + final LinearLayout layout = view.findViewById(R.id.autofill_save_custom_subtitle); + layout.addView(remote); + layout.setVisibility(View.VISIBLE); + } else { + Slog.w(TAG, "could not create remote presentation for custom title"); + } + } + switch (types.size()) { case 1: mTitle = Html.fromHtml(context.getString(R.string.autofill_save_title_with_type, @@ -162,9 +185,7 @@ final class SaveUi { subTitleView.setVisibility(View.VISIBLE); } - if (sDebug) { - Slog.d(TAG, "on constructor: title=" + mTitle + ", subTitle=" + mSubTitle); - } + if (sDebug) Slog.d(TAG, "on constructor: title=" + mTitle + ", subTitle=" + mSubTitle); final TextView noButton = view.findViewById(R.id.autofill_save_no); if (info.getNegativeActionStyle() == SaveInfo.NEGATIVE_BUTTON_STYLE_REJECT) { diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java index 4810f4fe8c82..f47b0d3c6e73 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java @@ -383,7 +383,7 @@ public class CompanionDeviceManagerService extends SystemService implements Bind findDeviceCallback, getServiceCallback()); } catch (RemoteException e) { - throw new RuntimeException(e); + Log.e(LOG_TAG, "Error while initiating device discovery", e); } } diff --git a/services/core/java/com/android/server/AppOpsService.java b/services/core/java/com/android/server/AppOpsService.java index c8e6e2efb240..29f8a1130792 100644 --- a/services/core/java/com/android/server/AppOpsService.java +++ b/services/core/java/com/android/server/AppOpsService.java @@ -16,22 +16,6 @@ package com.android.server; -import java.io.File; -import java.io.FileDescriptor; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.PrintWriter; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; - import android.Manifest; import android.app.ActivityManager; import android.app.ActivityThread; @@ -42,6 +26,7 @@ import android.content.pm.ApplicationInfo; import android.content.pm.IPackageManager; import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; +import android.content.pm.UserInfo; import android.media.AudioAttributes; import android.os.AsyncTask; import android.os.Binder; @@ -55,6 +40,7 @@ import android.os.ServiceManager; import android.os.ShellCallback; import android.os.ShellCommand; import android.os.UserHandle; +import android.os.UserManager; import android.os.storage.StorageManagerInternal; import android.util.ArrayMap; import android.util.ArraySet; @@ -66,8 +52,8 @@ import android.util.SparseIntArray; import android.util.TimeUtils; import android.util.Xml; -import com.android.internal.app.IAppOpsService; import com.android.internal.app.IAppOpsCallback; +import com.android.internal.app.IAppOpsService; import com.android.internal.os.Zygote; import com.android.internal.util.ArrayUtils; import com.android.internal.util.DumpUtils; @@ -76,10 +62,27 @@ import com.android.internal.util.Preconditions; import com.android.internal.util.XmlUtils; import libcore.util.EmptyArray; + import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlSerializer; +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + public class AppOpsService extends IAppOpsService.Stub { static final String TAG = "AppOps"; static final boolean DEBUG = false; @@ -2526,38 +2529,56 @@ public class AppOpsService extends IAppOpsService.Stub { perUserRestrictions = new SparseArray<>(); } - if (perUserRestrictions != null) { - boolean[] userRestrictions = perUserRestrictions.get(userId); - if (userRestrictions == null && restricted) { - userRestrictions = new boolean[AppOpsManager._NUM_OP]; - perUserRestrictions.put(userId, userRestrictions); - } - if (userRestrictions != null && userRestrictions[code] != restricted) { - userRestrictions[code] = restricted; - if (!restricted && isDefault(userRestrictions)) { - perUserRestrictions.remove(userId); - userRestrictions = null; - } - changed = true; + int[] users; + if (userId == UserHandle.USER_ALL) { + List<UserInfo> liveUsers = UserManager.get(mContext).getUsers(false); + + users = new int[liveUsers.size()]; + for (int i = 0; i < liveUsers.size(); i++) { + users[i] = liveUsers.get(i).id; } + } else { + users = new int[]{userId}; + } - if (userRestrictions != null) { - final boolean noExcludedPackages = ArrayUtils.isEmpty(excludedPackages); - if (perUserExcludedPackages == null && !noExcludedPackages) { - perUserExcludedPackages = new SparseArray<>(); + if (perUserRestrictions != null) { + int numUsers = users.length; + + for (int i = 0; i < numUsers; i++) { + int thisUserId = users[i]; + + boolean[] userRestrictions = perUserRestrictions.get(thisUserId); + if (userRestrictions == null && restricted) { + userRestrictions = new boolean[AppOpsManager._NUM_OP]; + perUserRestrictions.put(thisUserId, userRestrictions); } - if (perUserExcludedPackages != null && !Arrays.equals(excludedPackages, - perUserExcludedPackages.get(userId))) { - if (noExcludedPackages) { - perUserExcludedPackages.remove(userId); - if (perUserExcludedPackages.size() <= 0) { - perUserExcludedPackages = null; - } - } else { - perUserExcludedPackages.put(userId, excludedPackages); + if (userRestrictions != null && userRestrictions[code] != restricted) { + userRestrictions[code] = restricted; + if (!restricted && isDefault(userRestrictions)) { + perUserRestrictions.remove(thisUserId); + userRestrictions = null; } changed = true; } + + if (userRestrictions != null) { + final boolean noExcludedPackages = ArrayUtils.isEmpty(excludedPackages); + if (perUserExcludedPackages == null && !noExcludedPackages) { + perUserExcludedPackages = new SparseArray<>(); + } + if (perUserExcludedPackages != null && !Arrays.equals(excludedPackages, + perUserExcludedPackages.get(thisUserId))) { + if (noExcludedPackages) { + perUserExcludedPackages.remove(thisUserId); + if (perUserExcludedPackages.size() <= 0) { + perUserExcludedPackages = null; + } + } else { + perUserExcludedPackages.put(thisUserId, excludedPackages); + } + changed = true; + } + } } } diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index b45c4bd78039..209c6be5dea8 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -20,6 +20,7 @@ import static android.Manifest.permission.CHANGE_CONFIGURATION; import static android.Manifest.permission.CHANGE_DEVICE_IDLE_TEMP_WHITELIST; import static android.Manifest.permission.INTERACT_ACROSS_USERS; import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL; +import static android.Manifest.permission.INTERNAL_SYSTEM_WINDOW; import static android.Manifest.permission.MANAGE_ACTIVITY_STACKS; import static android.Manifest.permission.READ_FRAME_BUFFER; import static android.Manifest.permission.START_TASKS_FROM_RECENTS; @@ -10320,7 +10321,7 @@ public class ActivityManagerService extends IActivityManager.Stub @Override public void moveStackToDisplay(int stackId, int displayId) { - enforceCallingPermission(MANAGE_ACTIVITY_STACKS, "moveStackToDisplay()"); + enforceCallingPermission(INTERNAL_SYSTEM_WINDOW, "moveStackToDisplay()"); synchronized (this) { final long ident = Binder.clearCallingIdentity(); @@ -12590,7 +12591,6 @@ public class ActivityManagerService extends IActivityManager.Stub Binder.restoreCallingIdentity(ident); } } - closeSystemDialogs("setLockScreenShown"); } @Override diff --git a/services/core/java/com/android/server/am/ActivityStackSupervisor.java b/services/core/java/com/android/server/am/ActivityStackSupervisor.java index 1ccac1b8dc8a..e0f2a751604a 100644 --- a/services/core/java/com/android/server/am/ActivityStackSupervisor.java +++ b/services/core/java/com/android/server/am/ActivityStackSupervisor.java @@ -16,6 +16,7 @@ package com.android.server.am; +import static android.Manifest.permission.INTERNAL_SYSTEM_WINDOW; import static android.Manifest.permission.MANAGE_ACTIVITY_STACKS; import static android.Manifest.permission.START_ANY_ACTIVITY; import static android.Manifest.permission.START_TASKS_FROM_RECENTS; @@ -38,11 +39,13 @@ import static android.app.ITaskStackListener.FORCED_RESIZEABLE_REASON_SPLIT_SCRE import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK; import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; import static android.content.pm.PackageManager.PERMISSION_GRANTED; +import static android.os.Process.SYSTEM_UID; import static android.os.Trace.TRACE_TAG_ACTIVITY_MANAGER; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.FLAG_PRIVATE; import static android.view.Display.INVALID_DISPLAY; import static android.view.Display.REMOVE_MODE_DESTROY_CONTENT; +import static android.view.Display.TYPE_VIRTUAL; import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.ACTION_PICTURE_IN_PICTURE_EXPANDED_TO_FULLSCREEN; import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_ALL; @@ -1694,6 +1697,24 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D return false; } + // Check if the caller can manage activity stacks. + final int startAnyPerm = mService.checkPermission(INTERNAL_SYSTEM_WINDOW, callingPid, + callingUid); + if (startAnyPerm == PERMISSION_GRANTED) { + if (DEBUG_TASKS) Slog.d(TAG, "Launch on display check:" + + " allow launch any on display"); + return true; + } + + if (activityDisplay.mDisplay.getType() == TYPE_VIRTUAL + && activityDisplay.mDisplay.getOwnerUid() != SYSTEM_UID) { + // Limit launching on virtual displays, because their contents can be read from Surface + // by apps that created them. + if (DEBUG_TASKS) Slog.d(TAG, "Launch on display check:" + + " disallow launch on virtual display for not-embedded activity"); + return false; + } + if (!activityDisplay.isPrivate()) { // Anyone can launch on a public display. if (DEBUG_TASKS) Slog.d(TAG, "Launch on display check:" @@ -1715,15 +1736,6 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D return true; } - // Check if the caller can manage activity stacks. - final int startAnyPerm = mService.checkPermission(MANAGE_ACTIVITY_STACKS, callingPid, - callingUid); - if (startAnyPerm == PERMISSION_GRANTED) { - if (DEBUG_TASKS) Slog.d(TAG, "Launch on display check:" - + " allow launch any on display"); - return true; - } - Slog.w(TAG, "Launch on display check: denied"); return false; } diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java index 50670710b044..5b6bf1fdbeb1 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java +++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java @@ -795,7 +795,7 @@ public class StatusBarManagerService extends IStatusBarService.Stub { mHandler.post(() -> { // ShutdownThread displays UI, so give it a UI context. if (safeMode) { - ShutdownThread.rebootSafeMode(getUiContext(), false); + ShutdownThread.rebootSafeMode(getUiContext(), true); } else { ShutdownThread.reboot(getUiContext(), PowerManager.SHUTDOWN_USER_REQUESTED, false); diff --git a/services/core/java/com/android/server/timezone/PackageStatusStorage.java b/services/core/java/com/android/server/timezone/PackageStatusStorage.java index 31f0e3145f8a..05e97c7ef1e9 100644 --- a/services/core/java/com/android/server/timezone/PackageStatusStorage.java +++ b/services/core/java/com/android/server/timezone/PackageStatusStorage.java @@ -16,73 +16,83 @@ package com.android.server.timezone; -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteOpenHelper; +import com.android.internal.util.FastXmlSerializer; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + +import android.util.AtomicFile; import android.util.Slog; +import android.util.Xml; import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.text.ParseException; import static com.android.server.timezone.PackageStatus.CHECK_COMPLETED_FAILURE; import static com.android.server.timezone.PackageStatus.CHECK_COMPLETED_SUCCESS; import static com.android.server.timezone.PackageStatus.CHECK_STARTED; +import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT; +import static org.xmlpull.v1.XmlPullParser.START_TAG; /** * Storage logic for accessing/mutating the Android system's persistent state related to time zone - * update checking. There is expected to be a single instance and all methods synchronized on - * {@code this} for thread safety. + * update checking. There is expected to be a single instance. All non-private methods are thread + * safe. */ final class PackageStatusStorage { - private static final String TAG = "timezone.PackageStatusStorage"; + private static final String LOG_TAG = "timezone.PackageStatusStorage"; - private static final String DATABASE_NAME = "timezonepackagestatus.db"; - private static final int DATABASE_VERSION = 1; - - /** The table name. It will have a single row with _id == {@link #SINGLETON_ID} */ - private static final String TABLE = "status"; - private static final String COLUMN_ID = "_id"; + private static final String TAG_PACKAGE_STATUS = "PackageStatus"; /** - * Column that stores a monotonically increasing lock ID, used to detect concurrent update + * Attribute that stores a monotonically increasing lock ID, used to detect concurrent update * issues without on-line locks. Incremented on every write. */ - private static final String COLUMN_OPTIMISTIC_LOCK_ID = "optimistic_lock_id"; + private static final String ATTRIBUTE_OPTIMISTIC_LOCK_ID = "optimisticLockId"; /** - * Column that stores the current "check status" of the time zone update application packages. + * Attribute that stores the current "check status" of the time zone update application + * packages. */ - private static final String COLUMN_CHECK_STATUS = "check_status"; + private static final String ATTRIBUTE_CHECK_STATUS = "checkStatus"; /** - * Column that stores the version of the time zone rules update application being checked / last - * checked. + * Attribute that stores the version of the time zone rules update application being checked + * / last checked. */ - private static final String COLUMN_UPDATE_APP_VERSION = "update_app_package_version"; + private static final String ATTRIBUTE_UPDATE_APP_VERSION = "updateAppPackageVersion"; /** - * Column that stores the version of the time zone rules data application being checked / last - * checked. + * Attribute that stores the version of the time zone rules data application being checked + * / last checked. */ - private static final String COLUMN_DATA_APP_VERSION = "data_app_package_version"; - - /** - * The ID of the one row. - */ - private static final int SINGLETON_ID = 1; + private static final String ATTRIBUTE_DATA_APP_VERSION = "dataAppPackageVersion"; private static final int UNKNOWN_PACKAGE_VERSION = -1; - private final DatabaseHelper mDatabaseHelper; + private final AtomicFile mPackageStatusFile; - PackageStatusStorage(Context context) { - mDatabaseHelper = new DatabaseHelper(context); + PackageStatusStorage(File storageDir) { + mPackageStatusFile = new AtomicFile(new File(storageDir, "packageStatus.xml")); + if (!mPackageStatusFile.getBaseFile().exists()) { + try { + insertInitialPackageStatus(); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } } - void deleteDatabaseForTests() { - SQLiteDatabase.deleteDatabase(mDatabaseHelper.getDatabaseFile()); + void deleteFileForTests() { + synchronized(this) { + mPackageStatusFile.delete(); + } } /** @@ -93,48 +103,60 @@ final class PackageStatusStorage { synchronized (this) { try { return getPackageStatusInternal(); - } catch (IllegalArgumentException e) { - // This means that data exists in the table but it was bad. - Slog.e(TAG, "Package status invalid, resetting and retrying", e); + } catch (ParseException e) { + // This means that data exists in the file but it was bad. + Slog.e(LOG_TAG, "Package status invalid, resetting and retrying", e); // Reset the storage so it is in a good state again. - mDatabaseHelper.recoverFromBadData(); - return getPackageStatusInternal(); + recoverFromBadData(e); + try { + return getPackageStatusInternal(); + } catch (ParseException e2) { + throw new IllegalStateException("Recovery from bad file failed", e2); + } } } } - private PackageStatus getPackageStatusInternal() { - String[] columns = { - COLUMN_CHECK_STATUS, COLUMN_UPDATE_APP_VERSION, COLUMN_DATA_APP_VERSION - }; - Cursor cursor = mDatabaseHelper.getReadableDatabase() - .query(TABLE, columns, COLUMN_ID + " = ?", - new String[] { Integer.toString(SINGLETON_ID) }, - null /* groupBy */, null /* having */, null /* orderBy */); - if (cursor.getCount() != 1) { - Slog.e(TAG, "Unable to find package status from package status row. Rows returned: " - + cursor.getCount()); - return null; + private PackageStatus getPackageStatusInternal() throws ParseException { + try (FileInputStream fis = mPackageStatusFile.openRead()) { + XmlPullParser parser = parseToPackageStatusTag(fis); + Integer checkStatus = getNullableIntAttribute(parser, ATTRIBUTE_CHECK_STATUS); + if (checkStatus == null) { + return null; + } + int updateAppVersion = getIntAttribute(parser, ATTRIBUTE_UPDATE_APP_VERSION); + int dataAppVersion = getIntAttribute(parser, ATTRIBUTE_DATA_APP_VERSION); + return new PackageStatus(checkStatus, + new PackageVersions(updateAppVersion, dataAppVersion)); + } catch (IOException e) { + ParseException e2 = new ParseException("Error reading package status", 0); + e2.initCause(e); + throw e2; } - cursor.moveToFirst(); + } - // Determine check status. - if (cursor.isNull(0)) { - // This is normal the first time getPackageStatus() is called, or after - // resetCheckState(). - return null; + // Callers should be synchronized(this). + private int recoverFromBadData(Exception cause) { + mPackageStatusFile.delete(); + try { + return insertInitialPackageStatus(); + } catch (IOException e) { + IllegalStateException fatal = new IllegalStateException(e); + fatal.addSuppressed(cause); + throw fatal; } - int checkStatus = cursor.getInt(0); + } - // Determine package version. - if (cursor.isNull(1) || cursor.isNull(2)) { - Slog.e(TAG, "Package version information unexpectedly null"); - return null; - } - PackageVersions packageVersions = new PackageVersions(cursor.getInt(1), cursor.getInt(2)); + /** Insert the initial data, returning the optimistic lock ID */ + private int insertInitialPackageStatus() throws IOException { + // Doesn't matter what it is, but we avoid the obvious starting value each time the data + // is reset to ensure that old tokens are unlikely to work. + final int initialOptimisticLockId = (int) System.currentTimeMillis(); - return new PackageStatus(checkStatus, packageVersions); + writePackageStatusInternal(null /* status */, initialOptimisticLockId, + null /* packageVersions */); + return initialOptimisticLockId; } /** @@ -147,23 +169,29 @@ final class PackageStatusStorage { } synchronized (this) { - Integer optimisticLockId = getCurrentOptimisticLockId(); - if (optimisticLockId == null) { - Slog.w(TAG, "Unable to find optimistic lock ID from package status row"); + int optimisticLockId; + try { + optimisticLockId = getCurrentOptimisticLockId(); + } catch (ParseException e) { + Slog.w(LOG_TAG, "Unable to find optimistic lock ID from package status"); // Recover. - optimisticLockId = mDatabaseHelper.recoverFromBadData(); + optimisticLockId = recoverFromBadData(e); } int newOptimisticLockId = optimisticLockId + 1; - boolean statusRowUpdated = writeStatusRow( - optimisticLockId, newOptimisticLockId, CHECK_STARTED, currentInstalledVersions); - if (!statusRowUpdated) { - Slog.e(TAG, "Unable to update status to CHECK_STARTED in package status row." - + " synchronization failure?"); - return null; + try { + boolean statusUpdated = writePackageStatusWithOptimisticLockCheck( + optimisticLockId, newOptimisticLockId, CHECK_STARTED, + currentInstalledVersions); + if (!statusUpdated) { + throw new IllegalStateException("Unable to update status to CHECK_STARTED." + + " synchronization failure?"); + } + return new CheckToken(newOptimisticLockId, currentInstalledVersions); + } catch (IOException e) { + throw new IllegalStateException(e); } - return new CheckToken(newOptimisticLockId, currentInstalledVersions); } } @@ -172,19 +200,25 @@ final class PackageStatusStorage { */ void resetCheckState() { synchronized(this) { - Integer optimisticLockId = getCurrentOptimisticLockId(); - if (optimisticLockId == null) { - Slog.w(TAG, "resetCheckState: Unable to find optimistic lock ID from package" - + " status row"); + int optimisticLockId; + try { + optimisticLockId = getCurrentOptimisticLockId(); + } catch (ParseException e) { + Slog.w(LOG_TAG, "resetCheckState: Unable to find optimistic lock ID from package" + + " status"); // Attempt to recover the storage state. - optimisticLockId = mDatabaseHelper.recoverFromBadData(); + optimisticLockId = recoverFromBadData(e); } int newOptimisticLockId = optimisticLockId + 1; - if (!writeStatusRow(optimisticLockId, newOptimisticLockId, - null /* status */, null /* packageVersions */)) { - Slog.e(TAG, "resetCheckState: Unable to reset package status row," - + " newOptimisticLockId=" + newOptimisticLockId); + try { + if (!writePackageStatusWithOptimisticLockCheck(optimisticLockId, + newOptimisticLockId, null /* status */, null /* packageVersions */)) { + throw new IllegalStateException("resetCheckState: Unable to reset package" + + " status, newOptimisticLockId=" + newOptimisticLockId); + } + } catch (IOException e) { + throw new IllegalStateException(e); } } } @@ -199,138 +233,146 @@ final class PackageStatusStorage { int optimisticLockId = checkToken.mOptimisticLockId; int newOptimisticLockId = optimisticLockId + 1; int status = succeeded ? CHECK_COMPLETED_SUCCESS : CHECK_COMPLETED_FAILURE; - return writeStatusRow(optimisticLockId, newOptimisticLockId, - status, checkToken.mPackageVersions); + try { + return writePackageStatusWithOptimisticLockCheck(optimisticLockId, + newOptimisticLockId, status, checkToken.mPackageVersions); + } catch (IOException e) { + throw new IllegalStateException(e); + } } } - // Caller should be synchronized(this) - private Integer getCurrentOptimisticLockId() { - final String[] columns = { COLUMN_OPTIMISTIC_LOCK_ID }; - final String querySelection = COLUMN_ID + " = ?"; - final String[] querySelectionArgs = { Integer.toString(SINGLETON_ID) }; - - SQLiteDatabase database = mDatabaseHelper.getReadableDatabase(); - try (Cursor cursor = database.query(TABLE, columns, querySelection, querySelectionArgs, - null /* groupBy */, null /* having */, null /* orderBy */)) { - if (cursor.getCount() != 1) { - Slog.w(TAG, cursor.getCount() + " rows returned, expected exactly one."); - return null; - } - cursor.moveToFirst(); - return cursor.getInt(0); + // Caller should be synchronized(this). + private int getCurrentOptimisticLockId() throws ParseException { + try (FileInputStream fis = mPackageStatusFile.openRead()) { + XmlPullParser parser = parseToPackageStatusTag(fis); + return getIntAttribute(parser, ATTRIBUTE_OPTIMISTIC_LOCK_ID); + } catch (IOException e) { + ParseException e2 = new ParseException("Unable to read file", 0); + e2.initCause(e); + throw e2; } } - // Caller should be synchronized(this) - private boolean writeStatusRow(int optimisticLockId, int newOptimisticLockId, Integer status, - PackageVersions packageVersions) { - if ((status == null) != (packageVersions == null)) { - throw new IllegalArgumentException( - "Provide both status and packageVersions, or neither."); + /** Returns a parser or throws ParseException, never returns null. */ + private static XmlPullParser parseToPackageStatusTag(FileInputStream fis) + throws ParseException { + try { + XmlPullParser parser = Xml.newPullParser(); + parser.setInput(fis, StandardCharsets.UTF_8.name()); + int type; + while ((type = parser.next()) != END_DOCUMENT) { + final String tag = parser.getName(); + if (type == START_TAG && TAG_PACKAGE_STATUS.equals(tag)) { + return parser; + } + } + throw new ParseException("Unable to find " + TAG_PACKAGE_STATUS + " tag", 0); + } catch (XmlPullParserException e) { + throw new IllegalStateException("Unable to configure parser", e); + } catch (IOException e) { + ParseException e2 = new ParseException("Error reading XML", 0); + e.initCause(e); + throw e2; } + } - SQLiteDatabase database = mDatabaseHelper.getWritableDatabase(); - ContentValues values = new ContentValues(); - values.put(COLUMN_OPTIMISTIC_LOCK_ID, newOptimisticLockId); - if (status == null) { - values.putNull(COLUMN_CHECK_STATUS); - values.put(COLUMN_UPDATE_APP_VERSION, UNKNOWN_PACKAGE_VERSION); - values.put(COLUMN_DATA_APP_VERSION, UNKNOWN_PACKAGE_VERSION); - } else { - values.put(COLUMN_CHECK_STATUS, status); - values.put(COLUMN_UPDATE_APP_VERSION, packageVersions.mUpdateAppVersion); - values.put(COLUMN_DATA_APP_VERSION, packageVersions.mDataAppVersion); - } + // Caller should be synchronized(this). + private boolean writePackageStatusWithOptimisticLockCheck(int optimisticLockId, + int newOptimisticLockId, Integer status, PackageVersions packageVersions) + throws IOException { - String updateSelection = COLUMN_ID + " = ? AND " + COLUMN_OPTIMISTIC_LOCK_ID + " = ?"; - String[] updateSelectionArgs = { - Integer.toString(SINGLETON_ID), Integer.toString(optimisticLockId) - }; - int count = database.update(TABLE, values, updateSelection, updateSelectionArgs); - if (count > 1) { - // This has to be because of corruption: there should only ever be one row. - Slog.w(TAG, "writeStatusRow: " + count + " rows updated, expected exactly one."); - // Reset the table. - mDatabaseHelper.recoverFromBadData(); + int currentOptimisticLockId; + try { + currentOptimisticLockId = getCurrentOptimisticLockId(); + if (currentOptimisticLockId != optimisticLockId) { + return false; + } + } catch (ParseException e) { + recoverFromBadData(e); + return false; } - // 1 is the success case. 0 rows updated means the row is missing or the optimistic lock ID - // was not as expected, this could be because of corruption but is most likely due to an - // optimistic lock failure. Callers can decide on a case-by-case basis. - return count == 1; - } - - /** Only used during tests to force an empty table. */ - void deleteRowForTests() { - mDatabaseHelper.getWritableDatabase().delete(TABLE, null, null); + writePackageStatusInternal(status, newOptimisticLockId, packageVersions); + return true; } - /** Only used during tests to force a known table state. */ - public void forceCheckStateForTests(int checkStatus, PackageVersions packageVersions) { - int optimisticLockId = getCurrentOptimisticLockId(); - writeStatusRow(optimisticLockId, optimisticLockId, checkStatus, packageVersions); - } - - static class DatabaseHelper extends SQLiteOpenHelper { - - private final Context mContext; - - public DatabaseHelper(Context context) { - super(context, DATABASE_NAME, null, DATABASE_VERSION); - mContext = context; - } - - @Override - public void onCreate(SQLiteDatabase db) { - db.execSQL("CREATE TABLE " + TABLE + " (" + - "_id INTEGER PRIMARY KEY," + - COLUMN_OPTIMISTIC_LOCK_ID + " INTEGER NOT NULL," + - COLUMN_CHECK_STATUS + " INTEGER," + - COLUMN_UPDATE_APP_VERSION + " INTEGER NOT NULL," + - COLUMN_DATA_APP_VERSION + " INTEGER NOT NULL" + - ");"); - insertInitialRowState(db); + // Caller should be synchronized(this). + private void writePackageStatusInternal(Integer status, int optimisticLockId, + PackageVersions packageVersions) throws IOException { + if ((status == null) != (packageVersions == null)) { + throw new IllegalArgumentException( + "Provide both status and packageVersions, or neither."); } - @Override - public void onUpgrade(SQLiteDatabase db, int oldVersion, int currentVersion) { - // no-op: nothing to upgrade + FileOutputStream fos = null; + try { + fos = mPackageStatusFile.startWrite(); + XmlSerializer serializer = new FastXmlSerializer(); + serializer.setOutput(fos, StandardCharsets.UTF_8.name()); + serializer.startDocument(null /* encoding */, true /* standalone */); + final String namespace = null; + serializer.startTag(namespace, TAG_PACKAGE_STATUS); + String statusAttributeValue = status == null ? "" : Integer.toString(status); + serializer.attribute(namespace, ATTRIBUTE_CHECK_STATUS, statusAttributeValue); + serializer.attribute(namespace, ATTRIBUTE_OPTIMISTIC_LOCK_ID, + Integer.toString(optimisticLockId)); + int updateAppVersion = status == null + ? UNKNOWN_PACKAGE_VERSION : packageVersions.mUpdateAppVersion; + serializer.attribute(namespace, ATTRIBUTE_UPDATE_APP_VERSION, + Integer.toString(updateAppVersion)); + int dataAppVersion = status == null + ? UNKNOWN_PACKAGE_VERSION : packageVersions.mDataAppVersion; + serializer.attribute(namespace, ATTRIBUTE_DATA_APP_VERSION, + Integer.toString(dataAppVersion)); + serializer.endTag(namespace, TAG_PACKAGE_STATUS); + serializer.endDocument(); + serializer.flush(); + mPackageStatusFile.finishWrite(fos); + } catch (IOException e) { + if (fos != null) { + mPackageStatusFile.failWrite(fos); + } + throw e; } - /** Recover the initial data row state, returning the new current optimistic lock ID */ - int recoverFromBadData() { - // Delete the table content. - SQLiteDatabase writableDatabase = getWritableDatabase(); - writableDatabase.delete(TABLE, null /* whereClause */, null /* whereArgs */); + } - // Insert the initial content. - return insertInitialRowState(writableDatabase); + /** Only used during tests to force a known table state. */ + public void forceCheckStateForTests(int checkStatus, PackageVersions packageVersions) { + synchronized (this) { + try { + int optimisticLockId = getCurrentOptimisticLockId(); + writePackageStatusWithOptimisticLockCheck(optimisticLockId, optimisticLockId, + checkStatus, packageVersions); + } catch (IOException | ParseException e) { + throw new IllegalStateException(e); + } } + } - /** Insert the initial data row, returning the optimistic lock ID */ - private static int insertInitialRowState(SQLiteDatabase db) { - // Doesn't matter what it is, but we avoid the obvious starting value each time the row - // is reset to ensure that old tokens are unlikely to work. - final int initialOptimisticLockId = (int) System.currentTimeMillis(); - - // Insert the one row. - ContentValues values = new ContentValues(); - values.put(COLUMN_ID, SINGLETON_ID); - values.put(COLUMN_OPTIMISTIC_LOCK_ID, initialOptimisticLockId); - values.putNull(COLUMN_CHECK_STATUS); - values.put(COLUMN_UPDATE_APP_VERSION, UNKNOWN_PACKAGE_VERSION); - values.put(COLUMN_DATA_APP_VERSION, UNKNOWN_PACKAGE_VERSION); - long id = db.insert(TABLE, null, values); - if (id == -1) { - Slog.w(TAG, "insertInitialRow: could not insert initial row, id=" + id); - return -1; + private static Integer getNullableIntAttribute(XmlPullParser parser, String attributeName) + throws ParseException { + String attributeValue = parser.getAttributeValue(null, attributeName); + try { + if (attributeValue == null) { + throw new ParseException("Attribute " + attributeName + " missing", 0); + } else if (attributeValue.isEmpty()) { + return null; } - return initialOptimisticLockId; + return Integer.parseInt(attributeValue); + } catch (NumberFormatException e) { + throw new ParseException( + "Bad integer for attributeName=" + attributeName + ": " + attributeValue, 0); } + } - File getDatabaseFile() { - return mContext.getDatabasePath(DATABASE_NAME); + private static int getIntAttribute(XmlPullParser parser, String attributeName) + throws ParseException { + Integer value = getNullableIntAttribute(parser, attributeName); + if (value == null) { + throw new ParseException("Missing attribute " + attributeName, 0); } + return value; } } diff --git a/services/core/java/com/android/server/timezone/PackageTracker.java b/services/core/java/com/android/server/timezone/PackageTracker.java index 8abf7df9952b..f9af2eae92cd 100644 --- a/services/core/java/com/android/server/timezone/PackageTracker.java +++ b/services/core/java/com/android/server/timezone/PackageTracker.java @@ -21,9 +21,12 @@ import com.android.internal.annotations.VisibleForTesting; import android.app.timezone.RulesUpdaterContract; import android.content.Context; import android.content.pm.PackageManager; +import android.os.Environment; import android.provider.TimeZoneRulesDataContract; import android.util.Slog; +import java.io.File; + /** * Monitors the installed applications associated with time zone updates. If the app packages are * updated it indicates there <em>might</em> be a time zone rules update to apply so a targeted @@ -81,11 +84,17 @@ public class PackageTracker implements IntentHelper.Listener { /** Creates the {@link PackageTracker} for normal use. */ static PackageTracker create(Context context) { PackageTrackerHelperImpl helperImpl = new PackageTrackerHelperImpl(context); + // TODO(nfuller): Switch to FileUtils.createDir() when available. http://b/31008728 + File storageDir = new File(Environment.getDataSystemDirectory(), "timezone"); + if (!storageDir.exists()) { + storageDir.mkdir(); + } + return new PackageTracker( helperImpl /* clock */, helperImpl /* configHelper */, helperImpl /* packageManagerHelper */, - new PackageStatusStorage(context), + new PackageStatusStorage(storageDir), new IntentHelperImpl(context)); } diff --git a/services/core/java/com/android/server/wm/AppWindowContainerController.java b/services/core/java/com/android/server/wm/AppWindowContainerController.java index 5f34c6067997..4e4398ee9d91 100644 --- a/services/core/java/com/android/server/wm/AppWindowContainerController.java +++ b/services/core/java/com/android/server/wm/AppWindowContainerController.java @@ -614,7 +614,7 @@ public class AppWindowContainerController return STARTING_WINDOW_TYPE_SPLASH_SCREEN; } else if (taskSwitch && allowTaskSnapshot) { return snapshot == null ? STARTING_WINDOW_TYPE_NONE - : snapshotOrientationSameAsDisplay(snapshot) || fromRecents + : snapshotOrientationSameAsTask(snapshot) || fromRecents ? STARTING_WINDOW_TYPE_SNAPSHOT : STARTING_WINDOW_TYPE_SPLASH_SCREEN; } else { return STARTING_WINDOW_TYPE_NONE; @@ -640,24 +640,11 @@ public class AppWindowContainerController return true; } - private boolean snapshotOrientationSameAsDisplay(TaskSnapshot snapshot) { + private boolean snapshotOrientationSameAsTask(TaskSnapshot snapshot) { if (snapshot == null) { return false; } - final Rect rect = new Rect(0, 0, snapshot.getSnapshot().getWidth(), - snapshot.getSnapshot().getHeight()); - rect.inset(snapshot.getContentInsets()); - final Rect taskBoundsWithoutInsets = new Rect(); - mContainer.getTask().getBounds(taskBoundsWithoutInsets); - final DisplayInfo di = mContainer.getDisplayContent().getDisplayInfo(); - final Rect displayBounds = new Rect(0, 0, di.logicalWidth, di.logicalHeight); - final Rect stableInsets = new Rect(); - mService.mPolicy.getStableInsetsLw(di.rotation, di.logicalWidth, di.logicalHeight, - stableInsets); - displayBounds.inset(stableInsets); - final boolean snapshotInLandscape = rect.width() >= rect.height(); - final boolean displayInLandscape = displayBounds.width() >= displayBounds.height(); - return snapshotInLandscape == displayInLandscape; + return mContainer.getTask().getConfiguration().orientation == snapshot.getOrientation(); } public void removeStartingWindow() { diff --git a/services/core/java/com/android/server/wm/AppWindowToken.java b/services/core/java/com/android/server/wm/AppWindowToken.java index bd379344e18b..8afc4fd6a8df 100644 --- a/services/core/java/com/android/server/wm/AppWindowToken.java +++ b/services/core/java/com/android/server/wm/AppWindowToken.java @@ -937,8 +937,6 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree // Update keyguard flags upon finishing relaunch. checkKeyguardFlagsChanged(); } - - updateAllDrawn(); } void clearRelaunching() { @@ -1344,6 +1342,10 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree } } + /** + * Determines if the token has finished drawing. This should only be called from + * {@link DisplayContent#applySurfaceChangesTransaction} + */ void updateAllDrawn() { if (!allDrawn) { // Number of drawn windows can be less when a window is being relaunched, wait for diff --git a/services/core/jni/com_android_server_VibratorService.cpp b/services/core/jni/com_android_server_VibratorService.cpp index 804cd17fc27c..cb8416b36be5 100644 --- a/services/core/jni/com_android_server_VibratorService.cpp +++ b/services/core/jni/com_android_server_VibratorService.cpp @@ -42,107 +42,121 @@ using IVibrator_1_1 = android::hardware::vibrator::V1_1::IVibrator; namespace android { -static sp<IVibrator> mHal; +static constexpr int NUM_TRIES = 2; + +// Creates a Return<R> with STATUS::EX_NULL_POINTER. +template<class R> +inline Return<R> NullptrStatus() { + using ::android::hardware::Status; + return Return<R>{Status::fromExceptionCode(Status::EX_NULL_POINTER)}; +} + +// Helper used to transparently deal with the vibrator HAL becoming unavailable. +template<class R, class I, class... Args0, class... Args1> +Return<R> halCall(Return<R> (I::* fn)(Args0...), Args1&&... args1) { + // Assume that if getService returns a nullptr, HAL is not available on the + // device. + static sp<I> sHal = I::getService(); + static bool sAvailable = sHal != nullptr; + + if (!sAvailable) { + return NullptrStatus<R>(); + } + + // Return<R> doesn't have a default constructor, so make a Return<R> with + // STATUS::EX_NONE. + using ::android::hardware::Status; + Return<R> ret{Status::fromExceptionCode(Status::EX_NONE)}; + + // Note that ret is guaranteed to be changed after this loop. + for (int i = 0; i < NUM_TRIES; ++i) { + ret = (sHal == nullptr) ? NullptrStatus<R>() + : (*sHal.*fn)(std::forward<Args1>(args1)...); + + if (!ret.isOk()) { + ALOGE("Failed to issue command to vibrator HAL. Retrying."); + // Restoring connection to the HAL. + sHal = I::tryGetService(); + } + } + return ret; +} static void vibratorInit(JNIEnv /* env */, jobject /* clazz */) { - /* TODO(b/31632518) */ - if (mHal != nullptr) { - return; - } - mHal = IVibrator::getService(); + halCall(&IVibrator::ping).isOk(); } static jboolean vibratorExists(JNIEnv* /* env */, jobject /* clazz */) { - if (mHal != nullptr) { - return JNI_TRUE; - } else { - return JNI_FALSE; - } + return halCall(&IVibrator::ping).isOk() ? JNI_TRUE : JNI_FALSE; } static void vibratorOn(JNIEnv* /* env */, jobject /* clazz */, jlong timeout_ms) { - if (mHal != nullptr) { - Status retStatus = mHal->on(timeout_ms); - if (retStatus != Status::OK) { - ALOGE("vibratorOn command failed (%" PRIu32 ").", static_cast<uint32_t>(retStatus)); - } - } else { - ALOGW("Tried to vibrate but there is no vibrator device."); + Status retStatus = halCall(&IVibrator::on, timeout_ms).withDefault(Status::UNKNOWN_ERROR); + if (retStatus != Status::OK) { + ALOGE("vibratorOn command failed (%" PRIu32 ").", static_cast<uint32_t>(retStatus)); } } static void vibratorOff(JNIEnv* /* env */, jobject /* clazz */) { - if (mHal != nullptr) { - Status retStatus = mHal->off(); - if (retStatus != Status::OK) { - ALOGE("vibratorOff command failed (%" PRIu32 ").", static_cast<uint32_t>(retStatus)); - } - } else { - ALOGW("Tried to stop vibrating but there is no vibrator device."); + Status retStatus = halCall(&IVibrator::off).withDefault(Status::UNKNOWN_ERROR); + if (retStatus != Status::OK) { + ALOGE("vibratorOff command failed (%" PRIu32 ").", static_cast<uint32_t>(retStatus)); } } static jlong vibratorSupportsAmplitudeControl(JNIEnv*, jobject) { - if (mHal != nullptr) { - return mHal->supportsAmplitudeControl(); - } else { - ALOGW("Unable to get max vibration amplitude, there is no vibrator device."); - } - return false; + return halCall(&IVibrator::supportsAmplitudeControl).withDefault(false); } static void vibratorSetAmplitude(JNIEnv*, jobject, jint amplitude) { - if (mHal != nullptr) { - Status status = mHal->setAmplitude(static_cast<uint32_t>(amplitude)); - if (status != Status::OK) { - ALOGE("Failed to set vibrator amplitude (%" PRIu32 ").", - static_cast<uint32_t>(status)); - } - } else { - ALOGW("Unable to set vibration amplitude, there is no vibrator device."); + Status status = halCall(&IVibrator::setAmplitude, static_cast<uint32_t>(amplitude)) + .withDefault(Status::UNKNOWN_ERROR); + if (status != Status::OK) { + ALOGE("Failed to set vibrator amplitude (%" PRIu32 ").", + static_cast<uint32_t>(status)); } } static jlong vibratorPerformEffect(JNIEnv*, jobject, jlong effect, jint strength) { - if (mHal != nullptr) { - Status status; - uint32_t lengthMs; - auto callback = [&status, &lengthMs](Status retStatus, uint32_t retLengthMs) { - status = retStatus; - lengthMs = retLengthMs; - }; - EffectStrength effectStrength(static_cast<EffectStrength>(strength)); - - if (effect < 0 || effect > static_cast<uint32_t>(Effect_1_1::TICK)) { - ALOGW("Unable to perform haptic effect, invalid effect ID (%" PRId32 ")", + Status status; + uint32_t lengthMs; + auto callback = [&status, &lengthMs](Status retStatus, uint32_t retLengthMs) { + status = retStatus; + lengthMs = retLengthMs; + }; + EffectStrength effectStrength(static_cast<EffectStrength>(strength)); + + if (effect < 0 || effect > static_cast<uint32_t>(Effect_1_1::TICK)) { + ALOGW("Unable to perform haptic effect, invalid effect ID (%" PRId32 ")", + static_cast<int32_t>(effect)); + } else if (effect == static_cast<uint32_t>(Effect_1_1::TICK)) { + auto ret = halCall(&IVibrator_1_1::perform_1_1, static_cast<Effect_1_1>(effect), + effectStrength, callback); + if (!ret.isOk()) { + ALOGW("Failed to perform effect (%" PRId32 "), insufficient HAL version", static_cast<int32_t>(effect)); - } else if (effect == static_cast<uint32_t>(Effect_1_1::TICK)) { - sp<IVibrator_1_1> hal_1_1 = IVibrator_1_1::castFrom(mHal); - if (hal_1_1 != nullptr) { - hal_1_1->perform_1_1(static_cast<Effect_1_1>(effect), effectStrength, callback); - } else { - ALOGW("Failed to perform effect (%" PRId32 "), insufficient HAL version", - static_cast<int32_t>(effect)); - } - } else { - mHal->perform(static_cast<Effect>(effect), effectStrength, callback); - } - if (status == Status::OK) { - return lengthMs; - } else if (status != Status::UNSUPPORTED_OPERATION) { - // Don't warn on UNSUPPORTED_OPERATION, that's a normal even and just means the motor - // doesn't have a pre-defined waveform to perform for it, so we should just fall back - // to the framework waveforms. - ALOGE("Failed to perform haptic effect: effect=%" PRId64 ", strength=%" PRId32 - ", error=%" PRIu32 ").", static_cast<int64_t>(effect), - static_cast<int32_t>(strength), static_cast<uint32_t>(status)); } } else { - ALOGW("Unable to perform haptic effect, there is no vibrator device."); + auto ret = halCall(&IVibrator::perform, static_cast<Effect>(effect), effectStrength, + callback); + if (!ret.isOk()) { + ALOGW("Failed to perform effect (%" PRId32 ")", static_cast<int32_t>(effect)); + } + } + + if (status == Status::OK) { + return lengthMs; + } else if (status != Status::UNSUPPORTED_OPERATION) { + // Don't warn on UNSUPPORTED_OPERATION, that's a normal even and just means the motor + // doesn't have a pre-defined waveform to perform for it, so we should just fall back + // to the framework waveforms. + ALOGE("Failed to perform haptic effect: effect=%" PRId64 ", strength=%" PRId32 + ", error=%" PRIu32 ").", static_cast<int64_t>(effect), + static_cast<int32_t>(strength), static_cast<uint32_t>(status)); } return -1; } diff --git a/services/tests/notification/AndroidManifest.xml b/services/tests/notification/AndroidManifest.xml index 99d9c7b87301..c20020abb85c 100644 --- a/services/tests/notification/AndroidManifest.xml +++ b/services/tests/notification/AndroidManifest.xml @@ -31,7 +31,7 @@ </application> <instrumentation - android:name="android.support.test.runner.AndroidJUnitRunner" + android:name="android.testing.TestableInstrumentation" android:targetPackage="com.android.frameworks.tests.notification" android:label="Notification Tests" /> </manifest> diff --git a/services/tests/servicestests/src/com/android/server/timezone/PackageStatusStorageTest.java b/services/tests/servicestests/src/com/android/server/timezone/PackageStatusStorageTest.java index e085270fc3a4..dd56072372af 100644 --- a/services/tests/servicestests/src/com/android/server/timezone/PackageStatusStorageTest.java +++ b/services/tests/servicestests/src/com/android/server/timezone/PackageStatusStorageTest.java @@ -21,10 +21,11 @@ import org.junit.Before; import org.junit.Test; import android.content.Context; -import android.database.sqlite.SQLiteDatabase; import android.support.test.InstrumentationRegistry; import android.support.test.filters.SmallTest; +import java.io.File; + import static junit.framework.Assert.assertTrue; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -40,15 +41,16 @@ public class PackageStatusStorageTest { @Before public void setUp() throws Exception { Context context = InstrumentationRegistry.getContext(); + File dataDir = context.getFilesDir(); // Using the instrumentation context means the database is created in a test app-specific // directory. - mPackageStatusStorage = new PackageStatusStorage(context); + mPackageStatusStorage = new PackageStatusStorage(dataDir); } @After public void tearDown() throws Exception { - mPackageStatusStorage.deleteDatabaseForTests(); + mPackageStatusStorage.deleteFileForTests(); } @Test @@ -90,7 +92,7 @@ public class PackageStatusStorageTest { } @Test - public void generateCheckToken_missingRowBehavior() { + public void generateCheckToken_missingFileBehavior() { // Assert initial state. assertNull(mPackageStatusStorage.getPackageStatus()); @@ -100,15 +102,15 @@ public class PackageStatusStorageTest { // There should now be state. assertNotNull(mPackageStatusStorage.getPackageStatus()); - // Corrupt the table by removing the one row. - mPackageStatusStorage.deleteRowForTests(); + // Corrupt the data by removing the file. + mPackageStatusStorage.deleteFileForTests(); // Check that generateCheckToken recovers. assertNotNull(mPackageStatusStorage.generateCheckToken(VALID_PACKAGE_VERSIONS)); } @Test - public void getPackageStatus_missingRowBehavior() { + public void getPackageStatus_missingFileBehavior() { // Assert initial state. assertNull(mPackageStatusStorage.getPackageStatus()); @@ -118,14 +120,14 @@ public class PackageStatusStorageTest { // There should now be a state. assertNotNull(mPackageStatusStorage.getPackageStatus()); - // Corrupt the table by removing the one row. - mPackageStatusStorage.deleteRowForTests(); + // Corrupt the data by removing the file. + mPackageStatusStorage.deleteFileForTests(); assertNull(mPackageStatusStorage.getPackageStatus()); } @Test - public void markChecked_missingRowBehavior() { + public void markChecked_missingFileBehavior() { // Assert initial state. CheckToken token1 = mPackageStatusStorage.generateCheckToken(VALID_PACKAGE_VERSIONS); assertNotNull(token1); @@ -133,10 +135,10 @@ public class PackageStatusStorageTest { // There should now be a state. assertNotNull(mPackageStatusStorage.getPackageStatus()); - // Corrupt the table by removing the one row. - mPackageStatusStorage.deleteRowForTests(); + // Corrupt the data by removing the file. + mPackageStatusStorage.deleteFileForTests(); - // The missing row should mean token1 is now considered invalid, so we should get a false. + // The missing file should mean token1 is now considered invalid, so we should get a false. assertFalse(mPackageStatusStorage.markChecked(token1, true /* succeeded */)); // The storage should have recovered and we should be able to carry on like before. diff --git a/services/tests/servicestests/src/com/android/server/timezone/PackageTrackerTest.java b/services/tests/servicestests/src/com/android/server/timezone/PackageTrackerTest.java index 45b0af37f9e3..4c7680b1edef 100644 --- a/services/tests/servicestests/src/com/android/server/timezone/PackageTrackerTest.java +++ b/services/tests/servicestests/src/com/android/server/timezone/PackageTrackerTest.java @@ -71,7 +71,7 @@ public class PackageTrackerTest { // Using the instrumentation context means the database is created in a test app-specific // directory. We can use the real thing for this test. - mPackageStatusStorage = new PackageStatusStorage(context); + mPackageStatusStorage = new PackageStatusStorage(context.getFilesDir()); // For other interactions with the Android framework we create a fake object. mFakeIntentHelper = new FakeIntentHelper(); @@ -88,7 +88,7 @@ public class PackageTrackerTest { @After public void tearDown() throws Exception { if (mPackageStatusStorage != null) { - mPackageStatusStorage.deleteDatabaseForTests(); + mPackageStatusStorage.deleteFileForTests(); } } diff --git a/services/usb/java/com/android/server/usb/UsbPortManager.java b/services/usb/java/com/android/server/usb/UsbPortManager.java index e28513a29a3a..9d800ad5c479 100644 --- a/services/usb/java/com/android/server/usb/UsbPortManager.java +++ b/services/usb/java/com/android/server/usb/UsbPortManager.java @@ -692,7 +692,12 @@ public class UsbPortManager { // Guard against possible reentrance by posting the broadcast from the handler // instead of from within the critical section. - mHandler.post(() -> mContext.sendBroadcastAsUser(intent, UserHandle.ALL)); + mHandler.post(new Runnable() { + @Override + public void run() { + mContext.sendBroadcastAsUser(intent, UserHandle.ALL); + } + }); } private static void logAndPrint(int priority, IndentingPrintWriter pw, String msg) { diff --git a/telephony/java/android/telephony/MbmsDownloadManager.java b/telephony/java/android/telephony/MbmsDownloadManager.java index 79ee37a168d4..14ba7e5d274a 100644 --- a/telephony/java/android/telephony/MbmsDownloadManager.java +++ b/telephony/java/android/telephony/MbmsDownloadManager.java @@ -167,6 +167,14 @@ public class MbmsDownloadManager { "android.telephony.mbms.extra.TEMP_FILES_IN_USE"; /** + * Extra containing an instance of {@link android.telephony.mbms.ServiceInfo}, used by + * file-descriptor requests and cleanup requests to specify which service they want to + * request temp files or clean up temp files for, respectively. + */ + public static final String EXTRA_SERVICE_INFO = + "android.telephony.mbms.extra.SERVICE_INFO"; + + /** * Extra containing a single {@link Uri} indicating the location of the successfully * downloaded file. Set on the intent provided via * {@link android.telephony.mbms.DownloadRequest.Builder#setAppIntent(Intent)}. @@ -388,21 +396,10 @@ public class MbmsDownloadManager { tempRootDirectory.mkdirs(); setTempFileRootDirectory(tempRootDirectory); } - request.setAppName(mDownloadAppName); - // Check if the request is a multipart download. If so, validate that the destination is - // a directory that exists. - // TODO: figure out what qualifies a request as a multipart download request. - if (request.getSourceUri().getLastPathSegment() != null && - request.getSourceUri().getLastPathSegment().contains("*")) { - File toFile = new File(request.getDestinationUri().getSchemeSpecificPart()); - if (!toFile.isDirectory()) { - throw new IllegalArgumentException("Multipart download must specify valid " + - "destination directory."); - } - } - // TODO: check to make sure destination is clear - // TODO: write download request token + + checkValidDownloadDestination(request); + writeDownloadRequestToken(request); try { downloadService.download(request, callback); } catch (RemoteException e) { @@ -435,6 +432,7 @@ public class MbmsDownloadManager { * <li>ERROR_MSDC_UNKNOWN_REQUEST</li> */ public int cancelDownload(DownloadRequest downloadRequest) { + // TODO: don't forget to delete the token return 0; } @@ -518,4 +516,54 @@ public class MbmsDownloadManager { Log.i(LOG_TAG, "Remote exception while disposing of service"); } } + + private void writeDownloadRequestToken(DownloadRequest request) { + File tempFileLocation = MbmsUtils.getEmbmsTempFileDirForService(mContext, + request.getFileServiceInfo()); + if (!tempFileLocation.exists()) { + tempFileLocation.mkdirs(); + } + String downloadTokenFileName = request.getHash() + + MbmsDownloadReceiver.DOWNLOAD_TOKEN_SUFFIX; + File token = new File(tempFileLocation, downloadTokenFileName); + if (token.exists()) { + Log.w(LOG_TAG, "Download token " + downloadTokenFileName + " already exists"); + return; + } + try { + if (!token.createNewFile()) { + throw new RuntimeException("Failed to create download token for request " + + request); + } + } catch (IOException e) { + throw new RuntimeException("Failed to create download token for request " + request + + " due to IOException " + e); + } + } + + /** + * Verifies the following: + * If a request is multi-part, + * 1. Destination Uri must exist and be a directory + * 2. Directory specified must contain no files. + * Otherwise + * 1. The file specified by the destination Uri must not exist. + */ + private void checkValidDownloadDestination(DownloadRequest request) { + File toFile = new File(request.getDestinationUri().getSchemeSpecificPart()); + if (request.isMultipartDownload()) { + if (!toFile.isDirectory()) { + throw new IllegalArgumentException("Multipart download must specify valid " + + "destination directory."); + } + if (toFile.listFiles().length > 0) { + throw new IllegalArgumentException("Destination directory must be clear of all " + + "files."); + } + } else { + if (toFile.exists()) { + throw new IllegalArgumentException("Destination file must not exist."); + } + } + } } diff --git a/telephony/java/android/telephony/TelephonyScanManager.java b/telephony/java/android/telephony/TelephonyScanManager.java index c905d3a433f3..92a21b6fb85f 100644 --- a/telephony/java/android/telephony/TelephonyScanManager.java +++ b/telephony/java/android/telephony/TelephonyScanManager.java @@ -20,15 +20,18 @@ import static com.android.internal.util.Preconditions.checkNotNull; import android.content.Context; import android.os.Binder; +import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; import android.os.Messenger; +import android.os.Parcelable; import android.os.RemoteException; import android.os.ServiceManager; import android.util.Log; import android.util.SparseArray; +import java.util.Arrays; import java.util.List; import com.android.internal.telephony.ITelephony; @@ -42,6 +45,9 @@ public final class TelephonyScanManager { private static final String TAG = "TelephonyScanManager"; /** @hide */ + public static final String SCAN_RESULT_KEY = "scanResult"; + + /** @hide */ public static final int CALLBACK_SCAN_RESULTS = 1; /** @hide */ public static final int CALLBACK_SCAN_ERROR = 2; @@ -112,7 +118,13 @@ public final class TelephonyScanManager { switch (message.what) { case CALLBACK_SCAN_RESULTS: try { - callback.onResults((List<CellInfo>) message.obj); + final Bundle b = message.getData(); + final Parcelable[] parcelables = b.getParcelableArray(SCAN_RESULT_KEY); + CellInfo[] ci = new CellInfo[parcelables.length]; + for (int i = 0; i < parcelables.length; i++) { + ci[i] = (CellInfo) parcelables[i]; + } + callback.onResults((List<CellInfo>) Arrays.asList(ci)); } catch (Exception e) { Rlog.e(TAG, "Exception in networkscan callback onResults", e); } diff --git a/telephony/java/android/telephony/euicc/EuiccManager.java b/telephony/java/android/telephony/euicc/EuiccManager.java index f22a6327c80f..8304d84d8a2b 100644 --- a/telephony/java/android/telephony/euicc/EuiccManager.java +++ b/telephony/java/android/telephony/euicc/EuiccManager.java @@ -475,6 +475,36 @@ public class EuiccManager { } } + /** + * Ensure that subscriptions will be retained on the next factory reset. + * + * <p>By default, all subscriptions on the eUICC are erased the first time a device boots (ever + * and after factory resets). This ensures that the data is wiped after a factory reset is + * performed via fastboot or recovery mode, as these modes do not support the necessary radio + * communication needed to wipe the eSIM. + * + * <p>However, this method may be called right before a factory reset issued via settings when + * the user elects to retain subscriptions. Doing so will mark them for retention so that they + * are not cleared after the ensuing reset. + * + * <p>Requires that the calling app has the {@link android.Manifest.permission#MASTER_CLEAR} + * permission. This is for internal system use only. + * + * @param callbackIntent a PendingIntent to launch when the operation completes. + * @hide + */ + public void retainSubscriptionsForFactoryReset(PendingIntent callbackIntent) { + if (!isEnabled()) { + sendUnavailableError(callbackIntent); + return; + } + try { + mController.retainSubscriptionsForFactoryReset(callbackIntent); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + private static void sendUnavailableError(PendingIntent callbackIntent) { try { callbackIntent.send(EMBEDDED_SUBSCRIPTION_RESULT_ERROR); diff --git a/telephony/java/android/telephony/mbms/DownloadRequest.java b/telephony/java/android/telephony/mbms/DownloadRequest.java index c561741cc80c..907b0cbd8004 100644 --- a/telephony/java/android/telephony/mbms/DownloadRequest.java +++ b/telephony/java/android/telephony/mbms/DownloadRequest.java @@ -20,15 +20,22 @@ import android.content.Intent; import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; +import android.util.Base64; import java.lang.IllegalStateException; import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; /** * A Parcelable class describing a pending Cell-Broadcast download request * @hide */ public class DownloadRequest implements Parcelable { + // Version code used to keep token calculation consistent. + private static final int CURRENT_VERSION = 1; + /** @hide */ public static class Builder { private int id; @@ -37,6 +44,7 @@ public class DownloadRequest implements Parcelable { private Uri dest; private int subscriptionId; private String appIntent; + private int version = CURRENT_VERSION; public Builder setId(int id) { this.id = id; @@ -68,9 +76,14 @@ public class DownloadRequest implements Parcelable { return this; } + public Builder setVersion(int version) { + this.version = version; + return this; + } + public DownloadRequest build() { return new DownloadRequest(id, serviceInfo, source, dest, - subscriptionId, appIntent, null); + subscriptionId, appIntent, null, version); } } @@ -80,11 +93,12 @@ public class DownloadRequest implements Parcelable { private final Uri destinationUri; private final int subscriptionId; private final String serializedResultIntentForApp; + private final int version; private String appName; // not the Android app Name, the embms app name private DownloadRequest(int id, FileServiceInfo serviceInfo, Uri source, Uri dest, - int sub, String appIntent, String name) { + int sub, String appIntent, String name, int version) { downloadId = id; fileServiceInfo = serviceInfo; sourceUri = source; @@ -92,6 +106,7 @@ public class DownloadRequest implements Parcelable { subscriptionId = sub; serializedResultIntentForApp = appIntent; appName = name; + this.version = version; } public static DownloadRequest copy(DownloadRequest other) { @@ -106,6 +121,7 @@ public class DownloadRequest implements Parcelable { subscriptionId = dr.subscriptionId; serializedResultIntentForApp = dr.serializedResultIntentForApp; appName = dr.appName; + version = dr.version; } private DownloadRequest(Parcel in) { @@ -116,6 +132,7 @@ public class DownloadRequest implements Parcelable { subscriptionId = in.readInt(); serializedResultIntentForApp = in.readString(); appName = in.readString(); + version = in.readInt(); } public int describeContents() { @@ -130,6 +147,7 @@ public class DownloadRequest implements Parcelable { out.writeInt(subscriptionId); out.writeString(serializedResultIntentForApp); out.writeString(appName); + out.writeInt(version); } public int getDownloadId() { @@ -172,6 +190,10 @@ public class DownloadRequest implements Parcelable { return appName; } + public int getVersion() { + return version; + } + public static final Parcelable.Creator<DownloadRequest> CREATOR = new Parcelable.Creator<DownloadRequest>() { public DownloadRequest createFromParcel(Parcel in) { @@ -181,4 +203,35 @@ public class DownloadRequest implements Parcelable { return new DownloadRequest[size]; } }; + + /** + * @hide + */ + public boolean isMultipartDownload() { + // TODO: figure out what qualifies a request as a multipart download request. + return getSourceUri().getLastPathSegment() != null && + getSourceUri().getLastPathSegment().contains("*"); + } + + /** + * Retrieves the hash string that should be used as the filename when storing a token for + * this DownloadRequest. + * @hide + */ + public String getHash() { + MessageDigest digest; + try { + digest = MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("Could not get sha256 hash object"); + } + if (version >= 1) { + // Hash the source URI, destination URI, and the app intent + digest.update(sourceUri.toString().getBytes(StandardCharsets.UTF_8)); + digest.update(destinationUri.toString().getBytes(StandardCharsets.UTF_8)); + digest.update(serializedResultIntentForApp.getBytes(StandardCharsets.UTF_8)); + } + // Add updates for future versions here + return Base64.encodeToString(digest.digest(), Base64.URL_SAFE | Base64.NO_WRAP); + } } diff --git a/telephony/java/android/telephony/mbms/MbmsDownloadReceiver.java b/telephony/java/android/telephony/mbms/MbmsDownloadReceiver.java index b51c367deb36..1ce82d94b7e5 100644 --- a/telephony/java/android/telephony/mbms/MbmsDownloadReceiver.java +++ b/telephony/java/android/telephony/mbms/MbmsDownloadReceiver.java @@ -28,35 +28,85 @@ import android.telephony.MbmsDownloadManager; import android.util.Log; import java.io.File; +import java.io.FileFilter; +import java.io.FilenameFilter; import java.io.IOException; import java.util.ArrayList; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Objects; +import java.util.Set; import java.util.UUID; +import java.util.function.Predicate; /** * @hide */ public class MbmsDownloadReceiver extends BroadcastReceiver { + public static final String DOWNLOAD_TOKEN_SUFFIX = ".download_token"; + public static final String MBMS_FILE_PROVIDER_META_DATA_KEY = "mbms-file-provider-authority"; + + /** + * TODO: @SystemApi all these result codes + * Indicates that the requested operation completed without error. + */ + public static final int RESULT_OK = 0; + + /** + * Indicates that the intent sent had an invalid action. This will be the result if + * {@link Intent#getAction()} returns anything other than + * {@link MbmsDownloadManager#ACTION_DOWNLOAD_RESULT_INTERNAL}, + * {@link MbmsDownloadManager#ACTION_FILE_DESCRIPTOR_REQUEST}, or + * {@link MbmsDownloadManager#ACTION_CLEANUP}. + * This is a fatal result code and no result extras should be expected. + */ + public static final int RESULT_INVALID_ACTION = 1; + + /** + * Indicates that the intent was missing some required extras. + * This is a fatal result code and no result extras should be expected. + */ + public static final int RESULT_MALFORMED_INTENT = 2; + + /** + * Indicates that the supplied value for {@link MbmsDownloadManager#EXTRA_TEMP_FILE_ROOT} + * does not match what the app has stored. + * This is a fatal result code and no result extras should be expected. + */ + public static final int RESULT_BAD_TEMP_FILE_ROOT = 3; + + /** + * Indicates that the manager was unable to move the completed download to its final location. + * This is a fatal result code and no result extras should be expected. + */ + public static final int RESULT_DOWNLOAD_FINALIZATION_ERROR = 4; + + /** + * Indicates that the manager weas unable to generate one or more of the requested file + * descriptors. + * This is a non-fatal result code -- some file descriptors may still be generated, but there + * is no guarantee that they will be the same number as requested. + */ + public static final int RESULT_TEMP_FILE_GENERATION_ERROR = 5; + private static final String LOG_TAG = "MbmsDownloadReceiver"; private static final String TEMP_FILE_SUFFIX = ".embms.temp"; private static final int MAX_TEMP_FILE_RETRIES = 5; - public static final String MBMS_FILE_PROVIDER_META_DATA_KEY = "mbms-file-provider-authority"; private String mFileProviderAuthorityCache = null; private String mMiddlewarePackageNameCache = null; @Override public void onReceive(Context context, Intent intent) { - if (!verifyIntentContents(intent)) { - setResultCode(1 /* TODO: define error constants */); + if (!verifyIntentContents(context, intent)) { + setResultCode(RESULT_MALFORMED_INTENT); return; } if (!Objects.equals(intent.getStringExtra(MbmsDownloadManager.EXTRA_TEMP_FILE_ROOT), MbmsTempFileProvider.getEmbmsTempFileDir(context).getPath())) { - setResultCode(1 /* TODO: define error constants */); + setResultCode(RESULT_BAD_TEMP_FILE_ROOT); return; } @@ -65,11 +115,14 @@ public class MbmsDownloadReceiver extends BroadcastReceiver { cleanupPostMove(context, intent); } else if (MbmsDownloadManager.ACTION_FILE_DESCRIPTOR_REQUEST.equals(intent.getAction())) { generateTempFiles(context, intent); + } else if (MbmsDownloadManager.ACTION_CLEANUP.equals(intent.getAction())) { + cleanupTempFiles(context, intent); + } else { + setResultCode(RESULT_INVALID_ACTION); } - // TODO: Add handling for ACTION_CLEANUP } - private boolean verifyIntentContents(Intent intent) { + private boolean verifyIntentContents(Context context, Intent intent) { if (MbmsDownloadManager.ACTION_DOWNLOAD_RESULT_INTERNAL.equals(intent.getAction())) { if (!intent.hasExtra(MbmsDownloadManager.EXTRA_RESULT)) { Log.w(LOG_TAG, "Download result did not include a result code. Ignoring."); @@ -93,26 +146,47 @@ public class MbmsDownloadReceiver extends BroadcastReceiver { "temp file. Ignoring."); return false; } - return true; + DownloadRequest request = intent.getParcelableExtra(MbmsDownloadManager.EXTRA_REQUEST); + String expectedTokenFileName = request.getHash() + DOWNLOAD_TOKEN_SUFFIX; + File expectedTokenFile = new File( + MbmsUtils.getEmbmsTempFileDirForService(context, request.getFileServiceInfo()), + expectedTokenFileName); + if (!expectedTokenFile.exists()) { + Log.w(LOG_TAG, "Supplied download request does not match a token that we have. " + + "Expected " + expectedTokenFile); + return false; + } } else if (MbmsDownloadManager.ACTION_FILE_DESCRIPTOR_REQUEST.equals(intent.getAction())) { - if (!intent.hasExtra(MbmsDownloadManager.EXTRA_REQUEST)) { - Log.w(LOG_TAG, "Temp file request not include the associated request. Ignoring."); + if (!intent.hasExtra(MbmsDownloadManager.EXTRA_SERVICE_INFO)) { + Log.w(LOG_TAG, "Temp file request did not include the associated service info." + + " Ignoring."); return false; } if (!intent.hasExtra(MbmsDownloadManager.EXTRA_TEMP_FILE_ROOT)) { Log.w(LOG_TAG, "Download result did not include the temp file root. Ignoring."); return false; } - return true; + } else if (MbmsDownloadManager.ACTION_CLEANUP.equals(intent.getAction())) { + if (!intent.hasExtra(MbmsDownloadManager.EXTRA_SERVICE_INFO)) { + Log.w(LOG_TAG, "Cleanup request did not include the associated service info." + + " Ignoring."); + return false; + } + if (!intent.hasExtra(MbmsDownloadManager.EXTRA_TEMP_FILE_ROOT)) { + Log.w(LOG_TAG, "Cleanup request did not include the temp file root. Ignoring."); + return false; + } + if (!intent.hasExtra(MbmsDownloadManager.EXTRA_TEMP_FILES_IN_USE)) { + Log.w(LOG_TAG, "Cleanup request did not include the list of temp files in use. " + + "Ignoring."); + return false; + } } - - Log.w(LOG_TAG, "Received intent with unknown action: " + intent.getAction()); - return false; + return true; } private void moveDownloadedFile(Context context, Intent intent) { DownloadRequest request = intent.getParcelableExtra(MbmsDownloadManager.EXTRA_REQUEST); - // TODO: check request against token Intent intentForApp = request.getIntentForApp(); int result = intent.getIntExtra(MbmsDownloadManager.EXTRA_RESULT, @@ -127,9 +201,9 @@ public class MbmsDownloadReceiver extends BroadcastReceiver { Uri destinationUri = request.getDestinationUri(); Uri finalTempFile = intent.getParcelableExtra(MbmsDownloadManager.EXTRA_FINAL_URI); - if (!verifyTempFilePath(context, request, finalTempFile)) { + if (!verifyTempFilePath(context, request.getFileServiceInfo(), finalTempFile)) { Log.w(LOG_TAG, "Download result specified an invalid temp file " + finalTempFile); - setResultCode(1); + setResultCode(RESULT_DOWNLOAD_FINALIZATION_ERROR); return; } @@ -140,16 +214,15 @@ public class MbmsDownloadReceiver extends BroadcastReceiver { if (finalFileLocation == null) { Log.w(LOG_TAG, "Failed to move temp file to final destination"); // TODO: how do we notify the app of this? - setResultCode(1); + setResultCode(RESULT_DOWNLOAD_FINALIZATION_ERROR); } intentForApp.putExtra(MbmsDownloadManager.EXTRA_COMPLETED_FILE_URI, finalFileLocation); context.sendBroadcast(intentForApp); - setResultCode(0); + setResultCode(RESULT_OK); } private void cleanupPostMove(Context context, Intent intent) { - // TODO: account for in-use temp files DownloadRequest request = intent.getParcelableExtra(MbmsDownloadManager.EXTRA_REQUEST); if (request == null) { Log.w(LOG_TAG, "Intent does not include a DownloadRequest. Ignoring."); @@ -162,7 +235,7 @@ public class MbmsDownloadReceiver extends BroadcastReceiver { } for (Uri tempFileUri : tempFiles) { - if (verifyTempFilePath(context, request, tempFileUri)) { + if (verifyTempFilePath(context, request.getFileServiceInfo(), tempFileUri)) { File tempFile = new File(tempFileUri.getSchemeSpecificPart()); tempFile.delete(); } @@ -170,11 +243,12 @@ public class MbmsDownloadReceiver extends BroadcastReceiver { } private void generateTempFiles(Context context, Intent intent) { - // TODO: update pursuant to final decision on temp file locations - DownloadRequest request = intent.getParcelableExtra(MbmsDownloadManager.EXTRA_REQUEST); - if (request == null) { - Log.w(LOG_TAG, "Temp file request did not include the associated request. Ignoring."); - setResultCode(1 /* TODO: define error constants */); + FileServiceInfo serviceInfo = + intent.getParcelableExtra(MbmsDownloadManager.EXTRA_SERVICE_INFO); + if (serviceInfo == null) { + Log.w(LOG_TAG, "Temp file request did not include the associated service info. " + + "Ignoring."); + setResultCode(RESULT_MALFORMED_INTENT); return; } int fdCount = intent.getIntExtra(MbmsDownloadManager.EXTRA_FD_COUNT, 0); @@ -182,24 +256,27 @@ public class MbmsDownloadReceiver extends BroadcastReceiver { if (fdCount == 0 && (pausedList == null || pausedList.size() == 0)) { Log.i(LOG_TAG, "No temp files actually requested. Ending."); - setResultCode(0); + setResultCode(RESULT_OK); setResultExtras(Bundle.EMPTY); return; } - ArrayList<UriPathPair> freshTempFiles = generateFreshTempFiles(context, request, fdCount); + ArrayList<UriPathPair> freshTempFiles = + generateFreshTempFiles(context, serviceInfo, fdCount); ArrayList<UriPathPair> pausedFiles = - generateUrisForPausedFiles(context, request, pausedList); + generateUrisForPausedFiles(context, serviceInfo, pausedList); Bundle result = new Bundle(); result.putParcelableArrayList(MbmsDownloadManager.EXTRA_FREE_URI_LIST, freshTempFiles); result.putParcelableArrayList(MbmsDownloadManager.EXTRA_PAUSED_URI_LIST, pausedFiles); + setResultCode(RESULT_OK); setResultExtras(result); } - private ArrayList<UriPathPair> generateFreshTempFiles(Context context, DownloadRequest request, + private ArrayList<UriPathPair> generateFreshTempFiles(Context context, + FileServiceInfo serviceInfo, int freshFdCount) { - File tempFileDir = getEmbmsTempFileDirForRequest(context, request); + File tempFileDir = MbmsUtils.getEmbmsTempFileDirForService(context, serviceInfo); if (!tempFileDir.exists()) { tempFileDir.mkdirs(); } @@ -210,11 +287,11 @@ public class MbmsDownloadReceiver extends BroadcastReceiver { for (int i = 0; i < freshFdCount; i++) { File tempFile = generateSingleTempFile(tempFileDir); if (tempFile == null) { - setResultCode(2 /* TODO: define error constants */); + setResultCode(RESULT_TEMP_FILE_GENERATION_ERROR); Log.w(LOG_TAG, "Failed to generate a temp file. Moving on."); continue; } - Uri fileUri = Uri.fromParts(ContentResolver.SCHEME_FILE, tempFile.getPath(), null); + Uri fileUri = Uri.fromFile(tempFile); Uri contentUri = MbmsTempFileProvider.getUriForFile( context, getFileProviderAuthorityCached(context), tempFile); context.grantUriPermission(getMiddlewarePackageCached(context), contentUri, @@ -243,22 +320,22 @@ public class MbmsDownloadReceiver extends BroadcastReceiver { } private ArrayList<UriPathPair> generateUrisForPausedFiles(Context context, - DownloadRequest request, List<Uri> pausedFiles) { + FileServiceInfo serviceInfo, List<Uri> pausedFiles) { if (pausedFiles == null) { return new ArrayList<>(0); } ArrayList<UriPathPair> result = new ArrayList<>(pausedFiles.size()); for (Uri fileUri : pausedFiles) { - if (!verifyTempFilePath(context, request, fileUri)) { + if (!verifyTempFilePath(context, serviceInfo, fileUri)) { Log.w(LOG_TAG, "Supplied file " + fileUri + " is not a valid temp file to resume"); - setResultCode(2 /* TODO: define error codes */); + setResultCode(RESULT_TEMP_FILE_GENERATION_ERROR); continue; } File tempFile = new File(fileUri.getSchemeSpecificPart()); if (!tempFile.exists()) { Log.w(LOG_TAG, "Supplied file " + fileUri + " does not exist."); - setResultCode(2 /* TODO: define error codes */); + setResultCode(RESULT_TEMP_FILE_GENERATION_ERROR); continue; } Uri contentUri = MbmsTempFileProvider.getUriForFile( @@ -271,6 +348,38 @@ public class MbmsDownloadReceiver extends BroadcastReceiver { return result; } + private void cleanupTempFiles(Context context, Intent intent) { + FileServiceInfo serviceInfo = + intent.getParcelableExtra(MbmsDownloadManager.EXTRA_SERVICE_INFO); + File tempFileDir = MbmsUtils.getEmbmsTempFileDirForService(context, serviceInfo); + List<Uri> filesInUse = + intent.getParcelableArrayListExtra(MbmsDownloadManager.EXTRA_TEMP_FILES_IN_USE); + File[] filesToDelete = tempFileDir.listFiles(new FileFilter() { + @Override + public boolean accept(File file) { + File canonicalFile; + try { + canonicalFile = file.getCanonicalFile(); + } catch (IOException e) { + Log.w(LOG_TAG, "Got IOException canonicalizing " + file + ", not deleting."); + return false; + } + // Reject all files that don't match what we think a temp file should look like + // e.g. download tokens + if (!canonicalFile.getName().endsWith(TEMP_FILE_SUFFIX)) { + return false; + } + // If any of the files in use match the uri, return false to reject it from the + // list to delete. + Uri fileInUseUri = Uri.fromFile(canonicalFile); + return !filesInUse.contains(fileInUseUri); + } + }); + for (File fileToDelete : filesToDelete) { + fileToDelete.delete(); + } + } + private static String calculateDestinationFileRelativePath(DownloadRequest request, FileInfo info) { List<String> filePathComponents = info.getUri().getPathSegments(); @@ -330,7 +439,7 @@ public class MbmsDownloadReceiver extends BroadcastReceiver { return null; } - private static boolean verifyTempFilePath(Context context, DownloadRequest request, + private static boolean verifyTempFilePath(Context context, FileServiceInfo serviceInfo, Uri filePath) { // TODO: modify pursuant to final decision on temp file path scheme if (!ContentResolver.SCHEME_FILE.equals(filePath.getScheme())) { @@ -345,24 +454,14 @@ public class MbmsDownloadReceiver extends BroadcastReceiver { return false; } - if (!MbmsUtils.isContainedIn(getEmbmsTempFileDirForRequest(context, request), tempFile)) { + if (!MbmsUtils.isContainedIn( + MbmsUtils.getEmbmsTempFileDirForService(context, serviceInfo), tempFile)) { return false; } return true; } - /** - * Returns a File linked to the directory used to store temp files for this request - */ - private static File getEmbmsTempFileDirForRequest(Context context, DownloadRequest request) { - File embmsTempFileDir = MbmsTempFileProvider.getEmbmsTempFileDir(context); - - // TODO: better naming scheme for temp file dirs - String tempFileDirName = String.valueOf(request.getFileServiceInfo().getServiceId()); - return new File(embmsTempFileDir, tempFileDirName); - } - private String getFileProviderAuthorityCached(Context context) { if (mFileProviderAuthorityCache != null) { return mFileProviderAuthorityCache; diff --git a/telephony/java/android/telephony/mbms/MbmsUtils.java b/telephony/java/android/telephony/mbms/MbmsUtils.java index 7d4727563eee..b28950ec758f 100644 --- a/telephony/java/android/telephony/mbms/MbmsUtils.java +++ b/telephony/java/android/telephony/mbms/MbmsUtils.java @@ -85,4 +85,14 @@ public class MbmsUtils { context.bindService(bindIntent, serviceConnection, Context.BIND_AUTO_CREATE); } + + /** + * Returns a File linked to the directory used to store temp files for this file service + */ + public static File getEmbmsTempFileDirForService(Context context, FileServiceInfo serviceInfo) { + File embmsTempFileDir = MbmsTempFileProvider.getEmbmsTempFileDir(context); + + String tempFileDirName = String.valueOf(serviceInfo.getServiceId()); + return new File(embmsTempFileDir, tempFileDirName); + } } diff --git a/telephony/java/com/android/internal/telephony/euicc/IEuiccController.aidl b/telephony/java/com/android/internal/telephony/euicc/IEuiccController.aidl index fa43631cee9e..b3fc90db75d3 100644 --- a/telephony/java/com/android/internal/telephony/euicc/IEuiccController.aidl +++ b/telephony/java/com/android/internal/telephony/euicc/IEuiccController.aidl @@ -40,4 +40,5 @@ interface IEuiccController { oneway void updateSubscriptionNickname(int subscriptionId, String nickname, in PendingIntent callbackIntent); oneway void eraseSubscriptions(in PendingIntent callbackIntent); + oneway void retainSubscriptionsForFactoryReset(in PendingIntent callbackIntent); }
\ No newline at end of file diff --git a/tests/testables/src/android/testing/TestableInstrumentation.java b/tests/testables/src/android/testing/TestableInstrumentation.java new file mode 100644 index 000000000000..93fed859cf39 --- /dev/null +++ b/tests/testables/src/android/testing/TestableInstrumentation.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package android.testing; + +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.TestLooperManager; +import android.support.test.runner.AndroidJUnitRunner; +import android.util.Log; + +import java.util.ArrayList; + +/** + * Wrapper around instrumentation that spins up a TestLooperManager around + * the main looper whenever a test is not using it to attempt to stop crashes + * from stopping other tests from running. + */ +public class TestableInstrumentation extends AndroidJUnitRunner { + + private static final String TAG = "TestableInstrumentation"; + + private static final int MAX_CRASHES = 5; + private static MainLooperManager sManager; + + @Override + public void onCreate(Bundle arguments) { + sManager = new MainLooperManager(); + Log.setWtfHandler((tag, what, system) -> { + if (system) { + Log.e(TAG, "WTF!!", what); + } else { + // These normally kill the app, but we don't want that in a test, instead we want + // it to throw. + throw new RuntimeException(what); + } + }); + super.onCreate(arguments); + } + + @Override + public void finish(int resultCode, Bundle results) { + sManager.destroy(); + super.finish(resultCode, results); + } + + public static void acquireMain() { + if (sManager != null) { + sManager.acquireMain(); + } + } + + public static void releaseMain() { + if (sManager != null) { + sManager.releaseMain(); + } + } + + public class MainLooperManager implements Runnable { + + private final ArrayList<Throwable> mExceptions = new ArrayList<>(); + private Message mStopMessage; + private final Handler mMainHandler; + private TestLooperManager mManager; + + public MainLooperManager() { + mMainHandler = new Handler(Looper.getMainLooper()); + startManaging(); + } + + @Override + public void run() { + try { + synchronized (this) { + // Let the thing starting us know we are up and ready to run. + notify(); + } + while (true) { + Message m = mManager.next(); + if (m == mStopMessage) { + mManager.recycle(m); + return; + } + try { + mManager.execute(m); + } catch (Throwable t) { + if (!checkStack(t) || (mExceptions.size() == MAX_CRASHES)) { + throw t; + } + mExceptions.add(t); + Log.d(TAG, "Ignoring exception to run more tests", t); + } + mManager.recycle(m); + } + } finally { + mManager.release(); + synchronized (this) { + // Let the caller know we are done managing the main thread. + notify(); + } + } + } + + private boolean checkStack(Throwable t) { + StackTraceElement topStack = t.getStackTrace()[0]; + String className = topStack.getClassName(); + if (className.equals(TestLooperManager.class.getName())) { + topStack = t.getCause().getStackTrace()[0]; + className = topStack.getClassName(); + } + // Only interested in blocking exceptions from the app itself, not from android + // framework. + return !className.startsWith("android.") + && !className.startsWith("com.android.internal"); + } + + public void destroy() { + mStopMessage.sendToTarget(); + if (mExceptions.size() != 0) { + throw new RuntimeException("Exception caught during tests", mExceptions.get(0)); + } + } + + public void acquireMain() { + synchronized (this) { + mStopMessage.sendToTarget(); + try { + wait(); + } catch (InterruptedException e) { + } + } + } + + public void releaseMain() { + startManaging(); + } + + private void startManaging() { + mStopMessage = mMainHandler.obtainMessage(); + synchronized (this) { + mManager = acquireLooperManager(Looper.getMainLooper()); + // This bit needs to happen on a background thread or it will hang if called + // from the same thread we are looking to block. + new Thread(() -> { + // Post a message to the main handler that will manage executing all future + // messages. + mMainHandler.post(this); + while (!mManager.hasMessages(mMainHandler, null, this)); + // Lastly run the message that executes this so it can manage the main thread. + Message next = mManager.next(); + // Run through messages until we reach ours. + while (next.getCallback() != this) { + mManager.execute(next); + mManager.recycle(next); + next = mManager.next(); + } + mManager.execute(next); + }).start(); + if (Looper.myLooper() != Looper.getMainLooper()) { + try { + wait(); + } catch (InterruptedException e) { + } + } + } + } + } +} diff --git a/tests/testables/src/android/testing/TestableLooper.java b/tests/testables/src/android/testing/TestableLooper.java index f6c3cb3ec498..f1a70921a469 100644 --- a/tests/testables/src/android/testing/TestableLooper.java +++ b/tests/testables/src/android/testing/TestableLooper.java @@ -29,7 +29,6 @@ import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import java.lang.reflect.Field; import java.util.Map; /** @@ -49,7 +48,7 @@ public class TestableLooper { private TestLooperManager mQueueWrapper; public TestableLooper(Looper l) throws Exception { - this(InstrumentationRegistry.getInstrumentation().acquireLooperManager(l), l); + this(acquireLooperManager(l), l); } private TestableLooper(TestLooperManager wrapper, Looper l) throws Exception { @@ -78,6 +77,9 @@ public class TestableLooper { */ public void destroy() throws NoSuchFieldException, IllegalAccessException { mQueueWrapper.release(); + if (mLooper == Looper.getMainLooper()) { + TestableInstrumentation.releaseMain(); + } } /** @@ -196,6 +198,13 @@ public class TestableLooper { } } + private static TestLooperManager acquireLooperManager(Looper l) { + if (l == Looper.getMainLooper()) { + TestableInstrumentation.acquireMain(); + } + return InstrumentationRegistry.getInstrumentation().acquireLooperManager(l); + } + private static final Map<Object, TestableLooper> sLoopers = new ArrayMap<>(); /** @@ -247,8 +256,7 @@ public class TestableLooper { } boolean set = mTestableLooper.mQueueWrapper == null; if (set) { - mTestableLooper.mQueueWrapper = InstrumentationRegistry.getInstrumentation() - .acquireLooperManager(mLooper); + mTestableLooper.mQueueWrapper = acquireLooperManager(mLooper); } try { Object[] ret = new Object[1]; @@ -283,6 +291,9 @@ public class TestableLooper { if (set) { mTestableLooper.mQueueWrapper.release(); mTestableLooper.mQueueWrapper = null; + if (mLooper == Looper.getMainLooper()) { + TestableInstrumentation.releaseMain(); + } } } } diff --git a/tools/aapt2/cmd/Link.cpp b/tools/aapt2/cmd/Link.cpp index 740a401f9b57..e6bf3a6f9f56 100644 --- a/tools/aapt2/cmd/Link.cpp +++ b/tools/aapt2/cmd/Link.cpp @@ -989,7 +989,8 @@ class LinkCommand { manifest_class->GetCommentBuilder()->AppendComment(proper_annotation); } - const std::string& package_utf8 = context_->GetCompilationPackage(); + const std::string package_utf8 = + options_.custom_java_package.value_or_default(context_->GetCompilationPackage()); std::string out_path = options_.generate_java_class_path.value(); file::AppendPath(&out_path, file::PackageToPath(package_utf8)); diff --git a/tools/aapt2/java/ManifestClassGenerator_test.cpp b/tools/aapt2/java/ManifestClassGenerator_test.cpp index 5ebf508807e8..9f6ec210a6a7 100644 --- a/tools/aapt2/java/ManifestClassGenerator_test.cpp +++ b/tools/aapt2/java/ManifestClassGenerator_test.cpp @@ -18,124 +18,115 @@ #include "test/Test.h" -namespace aapt { +using ::testing::HasSubstr; +using ::testing::Not; -static ::testing::AssertionResult GetManifestClassText(IAaptContext* context, - xml::XmlResource* res, - std::string* out_str) { - std::unique_ptr<ClassDefinition> manifest_class = - GenerateManifestClass(context->GetDiagnostics(), res); - if (!manifest_class) { - return ::testing::AssertionFailure() << "manifest_class == nullptr"; - } - - std::stringstream out; - if (!manifest_class->WriteJavaFile(manifest_class.get(), "android", true, - &out)) { - return ::testing::AssertionFailure() << "failed to write java file"; - } +namespace aapt { - *out_str = out.str(); - return ::testing::AssertionSuccess(); -} +static ::testing::AssertionResult GetManifestClassText(IAaptContext* context, xml::XmlResource* res, + std::string* out_str); TEST(ManifestClassGeneratorTest, NameIsProperlyGeneratedFromSymbol) { std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build(); - std::unique_ptr<xml::XmlResource> manifest = test::BuildXmlDom(R"EOF( - <manifest xmlns:android="http://schemas.android.com/apk/res/android"> - <permission android:name="android.permission.ACCESS_INTERNET" /> - <permission android:name="android.DO_DANGEROUS_THINGS" /> - <permission android:name="com.test.sample.permission.HUH" /> - <permission-group android:name="foo.bar.PERMISSION" /> - </manifest>)EOF"); + std::unique_ptr<xml::XmlResource> manifest = test::BuildXmlDom(R"( + <manifest xmlns:android="http://schemas.android.com/apk/res/android"> + <permission android:name="android.permission.ACCESS_INTERNET" /> + <permission android:name="android.DO_DANGEROUS_THINGS" /> + <permission android:name="com.test.sample.permission.HUH" /> + <permission-group android:name="foo.bar.PERMISSION" /> + </manifest>)"); std::string actual; ASSERT_TRUE(GetManifestClassText(context.get(), manifest.get(), &actual)); - const size_t permission_class_pos = - actual.find("public static final class permission {"); - const size_t permission_croup_class_pos = + ASSERT_THAT(actual, HasSubstr("public static final class permission {")); + ASSERT_THAT(actual, HasSubstr("public static final class permission_group {")); + + const size_t permission_start_pos = actual.find("public static final class permission {"); + const size_t permission_group_start_pos = actual.find("public static final class permission_group {"); - ASSERT_NE(std::string::npos, permission_class_pos); - ASSERT_NE(std::string::npos, permission_croup_class_pos); // // Make sure these permissions are in the permission class. // - - size_t pos = actual.find( - "public static final String ACCESS_INTERNET=" - "\"android.permission.ACCESS_INTERNET\";"); - EXPECT_GT(pos, permission_class_pos); - EXPECT_LT(pos, permission_croup_class_pos); - - pos = actual.find( - "public static final String DO_DANGEROUS_THINGS=" - "\"android.DO_DANGEROUS_THINGS\";"); - EXPECT_GT(pos, permission_class_pos); - EXPECT_LT(pos, permission_croup_class_pos); - - pos = actual.find( - "public static final String HUH=\"com.test.sample.permission.HUH\";"); - EXPECT_GT(pos, permission_class_pos); - EXPECT_LT(pos, permission_croup_class_pos); + const std::string permission_class = + actual.substr(permission_start_pos, permission_group_start_pos - permission_start_pos); + + EXPECT_THAT( + permission_class, + HasSubstr( + "public static final String ACCESS_INTERNET=\"android.permission.ACCESS_INTERNET\";")); + EXPECT_THAT( + permission_class, + HasSubstr("public static final String DO_DANGEROUS_THINGS=\"android.DO_DANGEROUS_THINGS\";")); + EXPECT_THAT(permission_class, + HasSubstr("public static final String HUH=\"com.test.sample.permission.HUH\";")); // // Make sure these permissions are in the permission_group class // + const std::string permission_group_class = actual.substr(permission_group_start_pos); - pos = actual.find( - "public static final String PERMISSION=" - "\"foo.bar.PERMISSION\";"); - EXPECT_GT(pos, permission_croup_class_pos); - EXPECT_LT(pos, std::string::npos); + EXPECT_THAT(permission_group_class, + HasSubstr("public static final String PERMISSION=\"foo.bar.PERMISSION\";")); } TEST(ManifestClassGeneratorTest, CommentsAndAnnotationsArePresent) { std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build(); - std::unique_ptr<xml::XmlResource> manifest = test::BuildXmlDom(R"EOF( - <manifest xmlns:android="http://schemas.android.com/apk/res/android"> - <!-- Required to access the internet. - Added in API 1. --> - <permission android:name="android.permission.ACCESS_INTERNET" /> - <!-- @deprecated This permission is for playing outside. --> - <permission android:name="android.permission.PLAY_OUTSIDE" /> - <!-- This is a private permission for system only! - @hide - @SystemApi --> - <permission android:name="android.permission.SECRET" /> - </manifest>)EOF"); + std::unique_ptr<xml::XmlResource> manifest = test::BuildXmlDom(R"( + <manifest xmlns:android="http://schemas.android.com/apk/res/android"> + <!-- Required to access the internet. + Added in API 1. --> + <permission android:name="android.permission.ACCESS_INTERNET" /> + <!-- @deprecated This permission is for playing outside. --> + <permission android:name="android.permission.PLAY_OUTSIDE" /> + <!-- This is a private permission for system only! + @hide + @SystemApi --> + <permission android:name="android.permission.SECRET" /> + </manifest>)"); std::string actual; ASSERT_TRUE(GetManifestClassText(context.get(), manifest.get(), &actual)); - const char* expected_access_internet = - R"EOF( /** + const char* expected_access_internet = R"( /** * Required to access the internet. * Added in API 1. */ - public static final String ACCESS_INTERNET="android.permission.ACCESS_INTERNET";)EOF"; - - EXPECT_NE(std::string::npos, actual.find(expected_access_internet)); + public static final String ACCESS_INTERNET="android.permission.ACCESS_INTERNET";)"; + EXPECT_THAT(actual, HasSubstr(expected_access_internet)); - const char* expected_play_outside = - R"EOF( /** + const char* expected_play_outside = R"( /** * @deprecated This permission is for playing outside. */ @Deprecated - public static final String PLAY_OUTSIDE="android.permission.PLAY_OUTSIDE";)EOF"; - - EXPECT_NE(std::string::npos, actual.find(expected_play_outside)); + public static final String PLAY_OUTSIDE="android.permission.PLAY_OUTSIDE";)"; + EXPECT_THAT(actual, HasSubstr(expected_play_outside)); - const char* expected_secret = - R"EOF( /** + const char* expected_secret = R"( /** * This is a private permission for system only! * @hide */ @android.annotation.SystemApi - public static final String SECRET="android.permission.SECRET";)EOF"; + public static final String SECRET="android.permission.SECRET";)"; + EXPECT_THAT(actual, HasSubstr(expected_secret)); +} - EXPECT_NE(std::string::npos, actual.find(expected_secret)); +static ::testing::AssertionResult GetManifestClassText(IAaptContext* context, xml::XmlResource* res, + std::string* out_str) { + std::unique_ptr<ClassDefinition> manifest_class = + GenerateManifestClass(context->GetDiagnostics(), res); + if (!manifest_class) { + return ::testing::AssertionFailure() << "manifest_class == nullptr"; + } + + std::stringstream out; + if (!manifest_class->WriteJavaFile(manifest_class.get(), "android", true, &out)) { + return ::testing::AssertionFailure() << "failed to write java file"; + } + + *out_str = out.str(); + return ::testing::AssertionSuccess(); } } // namespace aapt diff --git a/tools/aapt2/java/ProguardRules.cpp b/tools/aapt2/java/ProguardRules.cpp index 624a559c4dae..5f61faeeebe7 100644 --- a/tools/aapt2/java/ProguardRules.cpp +++ b/tools/aapt2/java/ProguardRules.cpp @@ -85,7 +85,11 @@ class LayoutVisitor : public BaseVisitor { bool check_class = false; bool check_name = false; if (node->namespace_uri.empty()) { - check_class = node->name == "view" || node->name == "fragment"; + if (node->name == "view") { + check_class = true; + } else if (node->name == "fragment") { + check_class = check_name = true; + } } else if (node->namespace_uri == xml::kSchemaAndroid) { check_name = node->name == "fragment"; } @@ -110,6 +114,32 @@ class LayoutVisitor : public BaseVisitor { DISALLOW_COPY_AND_ASSIGN(LayoutVisitor); }; +class MenuVisitor : public BaseVisitor { + public: + MenuVisitor(const Source& source, KeepSet* keep_set) : BaseVisitor(source, keep_set) { + } + + virtual void Visit(xml::Element* node) override { + if (node->namespace_uri.empty() && node->name == "item") { + for (const auto& attr : node->attributes) { + if (attr.namespace_uri == xml::kSchemaAndroid) { + if ((attr.name == "actionViewClass" || attr.name == "actionProviderClass") && + util::IsJavaClassName(attr.value)) { + AddClass(node->line_number, attr.value); + } else if (attr.name == "onClick") { + AddMethod(node->line_number, attr.value); + } + } + } + } + + BaseVisitor::Visit(node); + } + + private: + DISALLOW_COPY_AND_ASSIGN(MenuVisitor); +}; + class XmlResourceVisitor : public BaseVisitor { public: XmlResourceVisitor(const Source& source, KeepSet* keep_set) @@ -267,6 +297,12 @@ bool CollectProguardRules(const Source& source, xml::XmlResource* res, break; } + case ResourceType::kMenu: { + MenuVisitor visitor(source, keep_set); + res->root->Accept(&visitor); + break; + } + default: break; } diff --git a/tools/aapt2/java/ProguardRules_test.cpp b/tools/aapt2/java/ProguardRules_test.cpp new file mode 100644 index 000000000000..900b07339715 --- /dev/null +++ b/tools/aapt2/java/ProguardRules_test.cpp @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "java/ProguardRules.h" + +#include "test/Test.h" + +using ::testing::HasSubstr; +using ::testing::Not; + +namespace aapt { + +TEST(ProguardRulesTest, FragmentNameRuleIsEmitted) { + std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build(); + std::unique_ptr<xml::XmlResource> layout = test::BuildXmlDom(R"( + <fragment xmlns:android="http://schemas.android.com/apk/res/android" + android:name="com.foo.Bar"/>)"); + layout->file.name = test::ParseNameOrDie("layout/foo"); + + proguard::KeepSet set; + ASSERT_TRUE(proguard::CollectProguardRules({}, layout.get(), &set)); + + std::stringstream out; + ASSERT_TRUE(proguard::WriteKeepSet(&out, set)); + + std::string actual = out.str(); + EXPECT_THAT(actual, HasSubstr("com.foo.Bar")); +} + +TEST(ProguardRulesTest, FragmentClassRuleIsEmitted) { + std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build(); + std::unique_ptr<xml::XmlResource> layout = + test::BuildXmlDom(R"(<fragment class="com.foo.Bar"/>)"); + layout->file.name = test::ParseNameOrDie("layout/foo"); + + proguard::KeepSet set; + ASSERT_TRUE(proguard::CollectProguardRules({}, layout.get(), &set)); + + std::stringstream out; + ASSERT_TRUE(proguard::WriteKeepSet(&out, set)); + + std::string actual = out.str(); + EXPECT_THAT(actual, HasSubstr("com.foo.Bar")); +} + +TEST(ProguardRulesTest, FragmentNameAndClassRulesAreEmitted) { + std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build(); + std::unique_ptr<xml::XmlResource> layout = test::BuildXmlDom(R"( + <fragment xmlns:android="http://schemas.android.com/apk/res/android" + android:name="com.foo.Baz" + class="com.foo.Bar"/>)"); + layout->file.name = test::ParseNameOrDie("layout/foo"); + + proguard::KeepSet set; + ASSERT_TRUE(proguard::CollectProguardRules({}, layout.get(), &set)); + + std::stringstream out; + ASSERT_TRUE(proguard::WriteKeepSet(&out, set)); + + std::string actual = out.str(); + EXPECT_THAT(actual, HasSubstr("com.foo.Bar")); + EXPECT_THAT(actual, HasSubstr("com.foo.Baz")); +} + +TEST(ProguardRulesTest, ViewOnClickRuleIsEmitted) { + std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build(); + std::unique_ptr<xml::XmlResource> layout = test::BuildXmlDom(R"( + <View xmlns:android="http://schemas.android.com/apk/res/android" + android:onClick="bar_method" />)"); + layout->file.name = test::ParseNameOrDie("layout/foo"); + + proguard::KeepSet set; + ASSERT_TRUE(proguard::CollectProguardRules({}, layout.get(), &set)); + + std::stringstream out; + ASSERT_TRUE(proguard::WriteKeepSet(&out, set)); + + std::string actual = out.str(); + EXPECT_THAT(actual, HasSubstr("bar_method")); +} + +TEST(ProguardRulesTest, MenuRulesAreEmitted) { + std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build(); + std::unique_ptr<xml::XmlResource> menu = test::BuildXmlDom(R"( + <menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:onClick="on_click" + android:actionViewClass="com.foo.Bar" + android:actionProviderClass="com.foo.Baz" + android:name="com.foo.Bat" /> + </menu>)"); + menu->file.name = test::ParseNameOrDie("menu/foo"); + + proguard::KeepSet set; + ASSERT_TRUE(proguard::CollectProguardRules({}, menu.get(), &set)); + + std::stringstream out; + ASSERT_TRUE(proguard::WriteKeepSet(&out, set)); + + std::string actual = out.str(); + EXPECT_THAT(actual, HasSubstr("on_click")); + EXPECT_THAT(actual, HasSubstr("com.foo.Bar")); + EXPECT_THAT(actual, HasSubstr("com.foo.Baz")); + EXPECT_THAT(actual, Not(HasSubstr("com.foo.Bat"))); +} + +} // namespace aapt diff --git a/tools/aapt2/link/ManifestFixer.cpp b/tools/aapt2/link/ManifestFixer.cpp index 99fd95be2571..a0ffefad1e1c 100644 --- a/tools/aapt2/link/ManifestFixer.cpp +++ b/tools/aapt2/link/ManifestFixer.cpp @@ -309,6 +309,9 @@ bool ManifestFixer::BuildRules(xml::XmlActionExecutor* executor, manifest_action["meta-data"] = meta_data_action; manifest_action["uses-split"].Action(RequiredNameIsJavaPackage); + manifest_action["key-sets"]["key-set"]["public-key"]; + manifest_action["key-sets"]["upgrade-key-set"]; + // Application actions. xml::XmlNodeAction& application_action = manifest_action["application"]; application_action.Action(OptionalNameIsJavaClassName); diff --git a/tools/aapt2/link/ManifestFixer_test.cpp b/tools/aapt2/link/ManifestFixer_test.cpp index 064d3650065d..80edb352f42c 100644 --- a/tools/aapt2/link/ManifestFixer_test.cpp +++ b/tools/aapt2/link/ManifestFixer_test.cpp @@ -18,7 +18,8 @@ #include "test/Test.h" -using android::StringPiece; +using ::android::StringPiece; +using ::testing::NotNull; namespace aapt { @@ -420,4 +421,22 @@ TEST_F(ManifestFixerTest, DoNotIgnoreNonNamespacedElements) { EXPECT_EQ(nullptr, Verify(input)); } +TEST_F(ManifestFixerTest, SupportKeySets) { + std::string input = R"( + <manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="android"> + <key-sets> + <key-set android:name="old-set"> + <public-key android:name="old-key" android:value="some+old+key" /> + </key-set> + <key-set android:name="new-set"> + <public-key android:name="new-key" android:value="some+new+key" /> + </key-set> + <upgrade-key-set android:name="old-set" /> + <upgrade-key-set android:name="new-set" /> + </key-sets> + </manifest>)"; + EXPECT_THAT(Verify(input), NotNull()); +} + } // namespace aapt diff --git a/tools/aapt2/readme.md b/tools/aapt2/readme.md index d6483ec1c0e8..ebcd4698d8d5 100644 --- a/tools/aapt2/readme.md +++ b/tools/aapt2/readme.md @@ -4,6 +4,11 @@ ### `aapt2 ...` - Fixed issue where enum values were interpreted as integers and range checked. (bug 62358540) - Fixed issue where ints and floats with trailing whitespace would not be parsed. (bug 62902869) +- Fixed issue where `--custom-package` was not honored when writing Manifest.java. (bug 62826426) +- Add `<key-sets>` and its nested tags to the allowed set of XML tags in AndroidManifest.xml. + (bug 62839863) +- Fixed issue where Java classes referenced from fragments and menus were not added to + the set of Proguard keep rules. (bug 62216174) ## Version 2.17 ### `aapt2 ...` |