diff options
46 files changed, 1525 insertions, 639 deletions
diff --git a/cmds/statsd/src/atoms.proto b/cmds/statsd/src/atoms.proto index 2899d49cf179..495a09f2e99a 100644 --- a/cmds/statsd/src/atoms.proto +++ b/cmds/statsd/src/atoms.proto @@ -48,6 +48,7 @@ import "frameworks/base/core/proto/android/stats/docsui/docsui_enums.proto"; import "frameworks/base/core/proto/android/stats/enums.proto"; import "frameworks/base/core/proto/android/stats/intelligence/enums.proto"; import "frameworks/base/core/proto/android/stats/launcher/launcher.proto"; +import "frameworks/base/core/proto/android/stats/location/location_enums.proto"; import "frameworks/base/core/proto/android/stats/mediametrics/mediametrics.proto"; import "frameworks/base/core/proto/android/stats/storage/storage_enums.proto"; import "frameworks/base/core/proto/android/stats/style/style_enums.proto"; @@ -301,6 +302,7 @@ message Atom { ContentCaptureServiceEvents content_capture_service_events = 207; ContentCaptureSessionEvents content_capture_session_events = 208; ContentCaptureFlushed content_capture_flushed = 209; + LocationManagerApiUsageReported location_manager_api_usage_reported = 210; } // Pulled events will start at field 10000. @@ -6485,3 +6487,60 @@ message AppOps { // while the app was in the background (only for trusted requests) optional int64 trusted_background_duration_millis = 9; } + +/** + * Location Manager API Usage information(e.g. API under usage, + * API call's parameters). + * Logged from: + * frameworks/base/services/core/java/com/android/server/LocationManagerService.java + */ +message LocationManagerApiUsageReported { + + // Indicating if usage starts or usage ends. + optional android.stats.location.UsageState state = 1; + + // LocationManagerService's API in use. + // We can identify which API from LocationManager is + // invoking current LMS API by the combination of + // API parameter(e.g. is_listener_null, is_intent_null, + // is_location_request_null) + optional android.stats.location.LocationManagerServiceApi api_in_use = 2; + + // Name of the package calling the API. + optional string calling_package_name = 3; + + // Type of the location provider. + optional android.stats.location.ProviderType provider = 4; + + // Quality of the location request + optional android.stats.location.LocationRequestQuality quality = 5; + + // The desired interval for active location updates, in milliseconds. + // Bucketized to reduce cardinality. + optional android.stats.location.LocationRequestIntervalBucket bucketized_interval = 6; + + // Minimum distance between location updates, in meters. + // Bucketized to reduce cardinality. + optional android.stats.location.SmallestDisplacementBucket + bucketized_smallest_displacement = 7; + + // The number of location updates. + optional int64 num_updates = 8; + + // The request expiration time, in millisecond since boot. + // Bucketized to reduce cardinality. + optional android.stats.location.ExpirationBucket + bucketized_expire_in = 9; + + // Type of Callback passed in for this API. + optional android.stats.location.CallbackType callback_type = 10; + + // The radius of the central point of the alert + // region, in meters. Only for API REQUEST_GEOFENCE. + // Bucketized to reduce cardinality. + optional android.stats.location.GeofenceRadiusBucket bucketized_radius = 11; + + // Activity Importance of API caller. + // Categorized to 3 types that are interesting from location's perspective. + optional android.stats.location.ActivityImportance activiy_importance = 12; +} diff --git a/core/java/android/app/admin/DevicePolicyEventLogger.java b/core/java/android/app/admin/DevicePolicyEventLogger.java index 44ea21881e88..95a797392940 100644 --- a/core/java/android/app/admin/DevicePolicyEventLogger.java +++ b/core/java/android/app/admin/DevicePolicyEventLogger.java @@ -22,9 +22,10 @@ import android.stats.devicepolicy.nano.StringList; import android.util.StatsLog; import com.android.framework.protobuf.nano.MessageNano; -import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.Preconditions; +import java.util.Arrays; + /** * A wrapper for logging managed device events using {@link StatsLog}. * <p/> @@ -45,7 +46,7 @@ import com.android.internal.util.Preconditions; * @see StatsLog * @hide */ -public final class DevicePolicyEventLogger { +public class DevicePolicyEventLogger { private final int mEventId; private int mIntValue; private boolean mBooleanValue; @@ -71,7 +72,6 @@ public final class DevicePolicyEventLogger { /** * Returns the event id. */ - @VisibleForTesting public int getEventId() { return mEventId; } @@ -87,7 +87,6 @@ public final class DevicePolicyEventLogger { /** * Returns the generic <code>int</code> value. */ - @VisibleForTesting public int getInt() { return mIntValue; } @@ -103,7 +102,6 @@ public final class DevicePolicyEventLogger { /** * Returns the generic <code>boolean</code> value. */ - @VisibleForTesting public boolean getBoolean() { return mBooleanValue; } @@ -119,7 +117,6 @@ public final class DevicePolicyEventLogger { /** * Returns the time period in milliseconds. */ - @VisibleForTesting public long getTimePeriod() { return mTimePeriodMs; } @@ -162,11 +159,13 @@ public final class DevicePolicyEventLogger { } /** - * Returns the generic <code>String[]</code> value. + * Returns a copy of the generic <code>String[]</code> value. */ - @VisibleForTesting public String[] getStringArray() { - return mStringArrayValue; + if (mStringArrayValue == null) { + return null; + } + return Arrays.copyOf(mStringArrayValue, mStringArrayValue.length); } /** @@ -188,7 +187,6 @@ public final class DevicePolicyEventLogger { /** * Returns the package name of the admin application. */ - @VisibleForTesting public String getAdminPackageName() { return mAdminPackageName; } diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java index ab630fd7467b..82d4d1d10d7e 100644 --- a/core/java/android/inputmethodservice/InputMethodService.java +++ b/core/java/android/inputmethodservice/InputMethodService.java @@ -472,8 +472,12 @@ public class InputMethodService extends AbstractInputMethodService { */ @MainThread @Override - public final void initializeInternal(IBinder token, int displayId, + public final void initializeInternal(@NonNull IBinder token, int displayId, IInputMethodPrivilegedOperations privilegedOperations) { + if (InputMethodPrivilegedOperationsRegistry.isRegistered(token)) { + Log.w(TAG, "The token has already registered, ignore this initialization."); + return; + } mPrivOps.set(privilegedOperations); InputMethodPrivilegedOperationsRegistry.put(token, mPrivOps); updateInputMethodDisplay(displayId); diff --git a/core/java/android/net/DnsResolver.java b/core/java/android/net/DnsResolver.java index 4b2b4c35b292..7a85dcbfc830 100644 --- a/core/java/android/net/DnsResolver.java +++ b/core/java/android/net/DnsResolver.java @@ -16,7 +16,7 @@ package android.net; -import static android.net.NetworkUtils.getDnsNetId; +import static android.net.NetworkUtils.getDnsNetwork; import static android.net.NetworkUtils.resNetworkCancel; import static android.net.NetworkUtils.resNetworkQuery; import static android.net.NetworkUtils.resNetworkResult; @@ -333,7 +333,7 @@ public final class DnsResolver { final Object lock = new Object(); final Network queryNetwork; try { - queryNetwork = (network != null) ? network : new Network(getDnsNetId()); + queryNetwork = (network != null) ? network : getDnsNetwork(); } catch (ErrnoException e) { executor.execute(() -> callback.onError(new DnsException(ERROR_SYSTEM, e))); return; @@ -433,7 +433,7 @@ public final class DnsResolver { final FileDescriptor queryfd; final Network queryNetwork; try { - queryNetwork = (network != null) ? network : new Network(getDnsNetId()); + queryNetwork = (network != null) ? network : getDnsNetwork(); queryfd = resNetworkQuery(queryNetwork.getNetIdForResolv(), domain, CLASS_IN, nsType, flags); } catch (ErrnoException e) { diff --git a/core/java/android/net/NetworkUtils.java b/core/java/android/net/NetworkUtils.java index a640f83ea5d2..d0f54b4c7f2d 100644 --- a/core/java/android/net/NetworkUtils.java +++ b/core/java/android/net/NetworkUtils.java @@ -158,10 +158,9 @@ public class NetworkUtils { /** * DNS resolver series jni method. - * Attempts to get netid of network which resolver will - * use if no network is explicitly selected. + * Attempts to get network which resolver will use if no network is explicitly selected. */ - public static native int getDnsNetId() throws ErrnoException; + public static native Network getDnsNetwork() throws ErrnoException; /** * Get the tcp repair window associated with the {@code fd}. diff --git a/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperationsRegistry.java b/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperationsRegistry.java index 1436aed5129a..049f952fceb8 100644 --- a/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperationsRegistry.java +++ b/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperationsRegistry.java @@ -132,4 +132,21 @@ public final class InputMethodPrivilegedOperationsRegistry { } } } + + /** + * Check the given IME token registration status. + * + * @param token IME token + * @return {@code true} when the IME token has already registered + * {@link InputMethodPrivilegedOperations}, {@code false} otherwise. + */ + @AnyThread + public static boolean isRegistered(IBinder token) { + synchronized (sLock) { + if (sRegistry == null) { + return false; + } + return sRegistry.containsKey(token); + } + } } diff --git a/core/jni/android_net_NetUtils.cpp b/core/jni/android_net_NetUtils.cpp index 00e0e3a74d70..08aa1d97fa1c 100644 --- a/core/jni/android_net_NetUtils.cpp +++ b/core/jni/android_net_NetUtils.cpp @@ -304,13 +304,19 @@ static void android_net_utils_resNetworkCancel(JNIEnv *env, jobject thiz, jobjec jniSetFileDescriptorOfFD(env, javaFd, -1); } -static jint android_net_utils_getDnsNetId(JNIEnv *env, jobject thiz) { - int dnsNetId = getNetworkForDns(); - if (dnsNetId < 0) { - throwErrnoException(env, "getDnsNetId", -dnsNetId); +static jobject android_net_utils_getDnsNetwork(JNIEnv *env, jobject thiz) { + unsigned dnsNetId = 0; + if (int res = getNetworkForDns(&dnsNetId) < 0) { + throwErrnoException(env, "getDnsNetId", -res); + return nullptr; } + bool privateDnsBypass = dnsNetId & NETID_USE_LOCAL_NAMESERVERS; - return dnsNetId; + static jclass class_Network = MakeGlobalRefOrDie( + env, FindClassOrDie(env, "android/net/Network")); + static jmethodID ctor = env->GetMethodID(class_Network, "<init>", "(IZ)V"); + return env->NewObject( + class_Network, ctor, dnsNetId & ~NETID_USE_LOCAL_NAMESERVERS, privateDnsBypass); } static jobject android_net_utils_getTcpRepairWindow(JNIEnv *env, jobject thiz, jobject javaFd) { @@ -369,7 +375,7 @@ static const JNINativeMethod gNetworkUtilMethods[] = { { "resNetworkQuery", "(ILjava/lang/String;III)Ljava/io/FileDescriptor;", (void*) android_net_utils_resNetworkQuery }, { "resNetworkResult", "(Ljava/io/FileDescriptor;)Landroid/net/DnsResolver$DnsResponse;", (void*) android_net_utils_resNetworkResult }, { "resNetworkCancel", "(Ljava/io/FileDescriptor;)V", (void*) android_net_utils_resNetworkCancel }, - { "getDnsNetId", "()I", (void*) android_net_utils_getDnsNetId }, + { "getDnsNetwork", "()Landroid/net/Network;", (void*) android_net_utils_getDnsNetwork }, }; int register_android_net_NetworkUtils(JNIEnv* env) diff --git a/core/proto/android/stats/location/location_enums.proto b/core/proto/android/stats/location/location_enums.proto new file mode 100644 index 000000000000..553c01c5d0dd --- /dev/null +++ b/core/proto/android/stats/location/location_enums.proto @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2019 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. + */ + +syntax = "proto2"; + +package android.stats.location; +option java_outer_classname = "LocationStatsEnums"; + + +// APIs from LocationManagerService +enum LocationManagerServiceApi { + API_UNKNOWN = 0; + API_REQUEST_LOCATION_UPDATES = 1; + API_ADD_GNSS_MEASUREMENTS_LISTENER = 2; + API_REGISTER_GNSS_STATUS_CALLBACK = 3; + API_REQUEST_GEOFENCE = 4; + API_SEND_EXTRA_COMMAND = 5; +} + +enum UsageState { + USAGE_STARTED = 0; + USAGE_ENDED = 1; +} + +// Type of location providers +enum ProviderType { + PROVIDER_UNKNOWN = 0; + PROVIDER_NETWORK = 1; + PROVIDER_GPS = 2; + PROVIDER_PASSIVE = 3; + PROVIDER_FUSED = 4; +} + +// Type of Callback passed in for this API +enum CallbackType { + CALLBACK_UNKNOWN = 0; + // Current API does not need a callback, e.g. sendExtraCommand + CALLBACK_NOT_APPLICABLE = 1; + CALLBACK_LISTENER = 2; + CALLBACK_PENDING_INTENT = 3; +} + +// Possible values for mQuality field in +// frameworks/base/location/java/android/location/LocationRequest.java +enum LocationRequestQuality { + QUALITY_UNKNOWN = 0; + ACCURACY_FINE = 100; + ACCURACY_BLOCK = 102; + ACCURACY_CITY = 104; + POWER_NONE = 200; + POWER_LOW = 201; + POWER_HIGH = 203; +} + +// Bucketized values for interval field in +// frameworks/base/location/java/android/location/LocationRequest.java +enum LocationRequestIntervalBucket { + INTERVAL_UNKNOWN = 0; + INTERVAL_BETWEEN_0_SEC_AND_1_SEC = 1; + INTERVAL_BETWEEN_1_SEC_AND_5_SEC = 2; + INTERVAL_BETWEEN_5_SEC_AND_1_MIN = 3; + INTERVAL_BETWEEN_1_MIN_AND_10_MIN = 4; + INTERVAL_BETWEEN_10_MIN_AND_1_HOUR = 5; + INTERVAL_LARGER_THAN_1_HOUR = 6; +} + +// Bucketized values for small displacement field in +// frameworks/base/location/java/android/location/LocationRequest.java +// Value in meters. +enum SmallestDisplacementBucket { + DISTANCE_UNKNOWN = 0; + DISTANCE_ZERO = 1; + DISTANCE_BETWEEN_0_AND_100 = 2; + DISTANCE_LARGER_THAN_100 = 3; +} + +// Bucketized values for expire_in field in +// frameworks/base/location/java/android/location/LocationRequest.java +enum ExpirationBucket { + EXPIRATION_UNKNOWN = 0; + EXPIRATION_BETWEEN_0_AND_20_SEC = 1; + EXPIRATION_BETWEEN_20_SEC_AND_1_MIN = 2; + EXPIRATION_BETWEEN_1_MIN_AND_10_MIN = 3; + EXPIRATION_BETWEEN_10_MIN_AND_1_HOUR = 4; + EXPIRATION_LARGER_THAN_1_HOUR = 5; + EXPIRATION_NO_EXPIRY = 6; +} + +// Bucketized values for radius field in +// frameworks/base/location/java/android/location/Geofence.java +// Value in meters. +enum GeofenceRadiusBucket { + RADIUS_UNKNOWN = 0; + RADIUS_BETWEEN_0_AND_100 = 1; + RADIUS_BETWEEN_100_AND_200 = 2; + RADIUS_BETWEEN_200_AND_300 = 3; + RADIUS_BETWEEN_300_AND_1000 = 4; + RADIUS_BETWEEN_1000_AND_10000 = 5; + RADIUS_LARGER_THAN_100000 = 6; + RADIUS_NEGATIVE = 7; +} + +// Caller Activity Importance. +enum ActivityImportance { + IMPORTANCE_UNKNOWN = 0; + IMPORTANCE_TOP = 1; + IMPORTANCE_FORGROUND_SERVICE = 2; + IMPORTANCE_BACKGROUND = 3; +} diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index fb585693c8a5..4fb343b9a37b 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -3306,6 +3306,10 @@ (which normally prevents seamless rotation). --> <bool name="config_allowSeamlessRotationDespiteNavBarMoving">false</bool> + <!-- Controls whether hints for gestural navigation are shown when the device is setup. + This should only be set when the device has gestural navigation enabled by default. --> + <bool name="config_showGesturalNavigationHints">false</bool> + <!-- Default insets [LEFT/RIGHTxTOP/BOTTOM] from the screen edge for picture-in-picture windows. These values are in DPs and will be converted to pixel sizes internally. --> <string translatable="false" name="config_defaultPictureInPictureScreenEdgeInsets">16x16</string> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 3d2d9693d62b..0bad05a8ff2d 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -2880,6 +2880,7 @@ <java-symbol type="bool" name="config_allowSeamlessRotationDespiteNavBarMoving" /> <java-symbol type="dimen" name="config_backGestureInset" /> <java-symbol type="color" name="system_bar_background_semi_transparent" /> + <java-symbol type="bool" name="config_showGesturalNavigationHints" /> <!-- EditText suggestion popup. --> <java-symbol type="id" name="suggestionWindowContainer" /> diff --git a/graphics/java/android/graphics/Outline.java b/graphics/java/android/graphics/Outline.java index 98c990a71601..1fc056c3652f 100644 --- a/graphics/java/android/graphics/Outline.java +++ b/graphics/java/android/graphics/Outline.java @@ -273,8 +273,12 @@ public final class Outline { } /** - * Sets the Constructs an Outline from a + * Sets the Outline to a * {@link android.graphics.Path#isConvex() convex path}. + * + * @param convexPath used to construct the Outline. As of + * {@link android.os.Build.VERSION_CODES#Q}, it is no longer required to be + * convex. */ public void setConvexPath(@NonNull Path convexPath) { if (convexPath.isEmpty()) { @@ -282,10 +286,6 @@ public final class Outline { return; } - if (!convexPath.isConvex()) { - throw new IllegalArgumentException("path must be convex"); - } - if (mPath == null) { mPath = new Path(); } diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java index 2541982e5a5a..2d6cd242c702 100644 --- a/media/java/android/media/AudioManager.java +++ b/media/java/android/media/AudioManager.java @@ -4274,38 +4274,6 @@ public class AudioManager { } } - /** - * Indicate A2DP source or sink active device change and eventually suppress - * the {@link AudioManager.ACTION_AUDIO_BECOMING_NOISY} intent. - * This operation is asynchronous but its execution will still be sequentially scheduled - * relative to calls to {@link #setBluetoothHearingAidDeviceConnectionState(BluetoothDevice, - * int, boolean, int)} and - * {@link #handleBluetoothA2dpDeviceConfigChange(BluetoothDevice)}. - * @param device Bluetooth device connected/disconnected - * @param state new connection state (BluetoothProfile.STATE_xxx) - * @param profile profile for the A2DP device - * (either {@link android.bluetooth.BluetoothProfile.A2DP} or - * {@link android.bluetooth.BluetoothProfile.A2DP_SINK}) - * @param a2dpVolume New volume for the connecting device. Does nothing if - * disconnecting. Pass value -1 in case you want this field to be ignored - * @param suppressNoisyIntent if true the - * {@link AudioManager.ACTION_AUDIO_BECOMING_NOISY} intent will not be sent. - * @return a delay in ms that the caller should wait before broadcasting - * BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED intent. - * {@hide} - */ - public void handleBluetoothA2dpActiveDeviceChange( - BluetoothDevice device, int state, int profile, - boolean suppressNoisyIntent, int a2dpVolume) { - final IAudioService service = getService(); - try { - service.handleBluetoothA2dpActiveDeviceChange(device, - state, profile, suppressNoisyIntent, a2dpVolume); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } - /** {@hide} */ public IRingtonePlayer getRingtonePlayer() { try { diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl index a790441aa36e..71f52a1b7d8e 100644 --- a/media/java/android/media/IAudioService.aidl +++ b/media/java/android/media/IAudioService.aidl @@ -178,9 +178,6 @@ interface IAudioService { void handleBluetoothA2dpDeviceConfigChange(in BluetoothDevice device); - void handleBluetoothA2dpActiveDeviceChange(in BluetoothDevice device, - int state, int profile, boolean suppressNoisyIntent, int a2dpVolume); - @UnsupportedAppUsage AudioRoutesInfo startWatchingRoutes(in IAudioRoutesObserver observer); diff --git a/packages/CarSystemUI/res/layout/notification_center_activity.xml b/packages/CarSystemUI/res/layout/notification_center_activity.xml index 55b0d875de41..0af74c4462a6 100644 --- a/packages/CarSystemUI/res/layout/notification_center_activity.xml +++ b/packages/CarSystemUI/res/layout/notification_center_activity.xml @@ -38,7 +38,7 @@ android:layout_width="0dp" android:layout_height="0dp" android:orientation="vertical" - android:paddingStart="@dimen/notification_shade_list_padding_bottom" + android:paddingBottom="@dimen/notification_shade_list_padding_bottom" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java index b6b34c760594..7fbdc2ed844b 100644 --- a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java +++ b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java @@ -389,22 +389,18 @@ public class CarStatusBar extends StatusBar implements } } }); + + // Attached to the Handle bar to close the notification shade + GestureDetector handleBarCloseNotificationGestureDetector = new GestureDetector(mContext, + new HandleBarCloseNotificationGestureListener()); + mNavBarNotificationTouchListener = (v, event) -> { boolean consumed = navBarCloseNotificationGestureDetector.onTouchEvent(event); if (consumed) { return true; } - if (event.getActionMasked() == MotionEvent.ACTION_UP - && mNotificationView.getVisibility() == View.VISIBLE) { - if (mSettleClosePercentage < mPercentageFromBottom) { - animateNotificationPanel( - DEFAULT_FLING_VELOCITY, false); - } else { - animateNotificationPanel(DEFAULT_FLING_VELOCITY, - true); - } - } + maybeCompleteAnimation(event); return true; }; @@ -418,15 +414,7 @@ public class CarStatusBar extends StatusBar implements if (consumed) { return true; } - if (event1.getActionMasked() == MotionEvent.ACTION_UP - && mNotificationView.getVisibility() == View.VISIBLE) { - if (mSettleOpenPercentage > mPercentageFromBottom) { - animateNotificationPanel(DEFAULT_FLING_VELOCITY, true); - } else { - animateNotificationPanel( - DEFAULT_FLING_VELOCITY, false); - } - } + maybeCompleteAnimation(event1); return true; } ); @@ -498,6 +486,13 @@ public class CarStatusBar extends StatusBar implements } return false; }); + + mHandleBar.setOnTouchListener((v, event) -> { + handleBarCloseNotificationGestureDetector.onTouchEvent(event); + maybeCompleteAnimation(event); + return true; + }); + mNotificationList = mNotificationView.findViewById(R.id.notifications); mNotificationList.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override @@ -612,6 +607,17 @@ public class CarStatusBar extends StatusBar implements setPanelExpanded(false); } + private void maybeCompleteAnimation(MotionEvent event) { + if (event.getActionMasked() == MotionEvent.ACTION_UP + && mNotificationView.getVisibility() == View.VISIBLE) { + if (mSettleClosePercentage < mPercentageFromBottom) { + animateNotificationPanel(DEFAULT_FLING_VELOCITY, false); + } else { + animateNotificationPanel(DEFAULT_FLING_VELOCITY, true); + } + } + } + /** * Animates the notification shade from one position to other. This is used to either open or * close the notification shade completely with a velocity. If the animation is to close the @@ -1054,8 +1060,10 @@ public class CarStatusBar extends StatusBar implements private static final int SWIPE_MAX_OFF_PATH = 75; private static final int SWIPE_THRESHOLD_VELOCITY = 200; - // Only responsible for open hooks. Since once the panel opens it covers all elements - // there is no need to merge with close. + /** + * Only responsible for open hooks. Since once the panel opens it covers all elements + * there is no need to merge with close. + */ private abstract class OpenNotificationGestureListener extends GestureDetector.SimpleOnGestureListener { @@ -1098,7 +1106,9 @@ public class CarStatusBar extends StatusBar implements protected abstract void openNotification(); } - // to be installed on the open panel notification panel + /** + * To be installed on the open panel notification panel + */ private abstract class CloseNotificationGestureListener extends GestureDetector.SimpleOnGestureListener { @@ -1172,7 +1182,9 @@ public class CarStatusBar extends StatusBar implements protected abstract void close(); } - // To be installed on the nav bars. + /** + * To be installed on the nav bars. + */ private abstract class NavBarCloseNotificationGestureListener extends CloseNotificationGestureListener { @Override @@ -1201,7 +1213,28 @@ public class CarStatusBar extends StatusBar implements } /** - * SystemUi version onf the notification manager that overrides methods such that the + * To be installed on the handle bar. + */ + private class HandleBarCloseNotificationGestureListener extends + GestureDetector.SimpleOnGestureListener { + + @Override + public boolean onScroll(MotionEvent event1, MotionEvent event2, float distanceX, + float distanceY) { + calculatePercentageFromBottom(event2.getRawY()); + // To prevent the jump in the clip bounds while closing the notification shade using + // the handle bar we should calculate the height using the diff of event1 and event2. + // This will help the notification shade to clip smoothly as the event2 value changes + // as event1 value will be fixed. + int clipHeight = + mNotificationView.getHeight() - (int) (event1.getRawY() - event2.getRawY()); + setNotificationViewClipBounds(clipHeight); + return true; + } + } + + /** + * SystemUi version of the notification manager that overrides methods such that the * notifications end up in the status bar layouts instead of a standalone window. */ private class CarSystemUIHeadsUpNotificationManager extends CarHeadsUpNotificationManager { diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index 94259416d274..91a8ab5f692f 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -58,6 +58,7 @@ android_library { "androidx.arch.core_core-runtime", "androidx.lifecycle_lifecycle-extensions", "androidx.dynamicanimation_dynamicanimation", + "androidx-constraintlayout_constraintlayout", "iconloader_base", "SystemUI-tags", "SystemUI-proto", @@ -111,6 +112,7 @@ android_library { "androidx.arch.core_core-runtime", "androidx.lifecycle_lifecycle-extensions", "androidx.dynamicanimation_dynamicanimation", + "androidx-constraintlayout_constraintlayout", "SystemUI-tags", "SystemUI-proto", "metrics-helper-lib", diff --git a/packages/SystemUI/res/layout/global_actions_grid.xml b/packages/SystemUI/res/layout/global_actions_grid.xml index 3928062e43d2..456553d404dc 100644 --- a/packages/SystemUI/res/layout/global_actions_grid.xml +++ b/packages/SystemUI/res/layout/global_actions_grid.xml @@ -1,73 +1,95 @@ <?xml version="1.0" encoding="utf-8"?> -<com.android.systemui.globalactions.GlobalActionsGridLayout +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@id/global_actions_view" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:id="@+id/global_actions_grid_root" android:layout_width="match_parent" android:layout_height="match_parent" - android:orientation="horizontal" - android:theme="@style/qs_theme" - android:gravity="bottom | center_horizontal" android:clipChildren="false" android:clipToPadding="false" - android:paddingBottom="@dimen/global_actions_grid_container_shadow_offset" android:layout_marginBottom="@dimen/global_actions_grid_container_negative_shadow_offset" > - <LinearLayout + + <FrameLayout + android:id="@+id/global_actions_panel_container" + android:layout_width="match_parent" + android:layout_height="0dp" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toTopOf="@id/global_actions_view" + /> + + <com.android.systemui.globalactions.GlobalActionsGridLayout + android:id="@id/global_actions_view" + android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_width="wrap_content" - android:layoutDirection="ltr" + android:orientation="horizontal" + android:theme="@style/qs_theme" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent" + android:gravity="bottom | center_horizontal" android:clipChildren="false" android:clipToPadding="false" - android:layout_marginBottom="@dimen/global_actions_grid_container_bottom_margin" + android:paddingBottom="@dimen/global_actions_grid_container_shadow_offset" + android:layout_marginBottom="@dimen/global_actions_grid_container_negative_shadow_offset" > - <!-- For separated items--> <LinearLayout - android:id="@+id/separated_button" - android:layout_width="wrap_content" - android:layout_height="match_parent" - android:layout_marginLeft="@dimen/global_actions_grid_side_margin" - android:layout_marginRight="@dimen/global_actions_grid_side_margin" - android:paddingLeft="@dimen/global_actions_grid_horizontal_padding" - android:paddingRight="@dimen/global_actions_grid_horizontal_padding" - android:paddingTop="@dimen/global_actions_grid_vertical_padding" - android:paddingBottom="@dimen/global_actions_grid_vertical_padding" - android:orientation="vertical" - android:gravity="center" - android:translationZ="@dimen/global_actions_translate" - /> - <!-- Grid of action items --> - <com.android.systemui.globalactions.ListGridLayout - android:id="@android:id/list" - android:layout_width="wrap_content" android:layout_height="wrap_content" - android:orientation="vertical" - android:gravity="right" - android:layout_marginRight="@dimen/global_actions_grid_side_margin" - android:translationZ="@dimen/global_actions_translate" - android:paddingLeft="@dimen/global_actions_grid_horizontal_padding" - android:paddingRight="@dimen/global_actions_grid_horizontal_padding" - android:paddingTop="@dimen/global_actions_grid_vertical_padding" - android:paddingBottom="@dimen/global_actions_grid_vertical_padding" + android:layout_width="wrap_content" + android:layoutDirection="ltr" + android:clipChildren="false" + android:clipToPadding="false" + android:layout_marginBottom="@dimen/global_actions_grid_container_bottom_margin" > + <!-- For separated items--> <LinearLayout + android:id="@+id/separated_button" android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:visibility="gone" - android:layoutDirection="locale" - /> - <LinearLayout - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:visibility="gone" - android:layoutDirection="locale" + android:layout_height="match_parent" + android:layout_marginLeft="@dimen/global_actions_grid_side_margin" + android:layout_marginRight="@dimen/global_actions_grid_side_margin" + android:paddingLeft="@dimen/global_actions_grid_horizontal_padding" + android:paddingRight="@dimen/global_actions_grid_horizontal_padding" + android:paddingTop="@dimen/global_actions_grid_vertical_padding" + android:paddingBottom="@dimen/global_actions_grid_vertical_padding" + android:orientation="vertical" + android:gravity="center" + android:translationZ="@dimen/global_actions_translate" /> - <LinearLayout + <!-- Grid of action items --> + <com.android.systemui.globalactions.ListGridLayout + android:id="@android:id/list" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:visibility="gone" - android:layoutDirection="locale" - /> - </com.android.systemui.globalactions.ListGridLayout> - </LinearLayout> + android:orientation="vertical" + android:gravity="right" + android:layout_marginRight="@dimen/global_actions_grid_side_margin" + android:translationZ="@dimen/global_actions_translate" + android:paddingLeft="@dimen/global_actions_grid_horizontal_padding" + android:paddingRight="@dimen/global_actions_grid_horizontal_padding" + android:paddingTop="@dimen/global_actions_grid_vertical_padding" + android:paddingBottom="@dimen/global_actions_grid_vertical_padding" + > + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:visibility="gone" + android:layoutDirection="locale" + /> + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:visibility="gone" + android:layoutDirection="locale" + /> + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:visibility="gone" + android:layoutDirection="locale" + /> + </com.android.systemui.globalactions.ListGridLayout> + </LinearLayout> -</com.android.systemui.globalactions.GlobalActionsGridLayout> + </com.android.systemui.globalactions.GlobalActionsGridLayout> +</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java index c63389a47d2d..771df2d86110 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java @@ -330,9 +330,7 @@ public class BubbleStackView extends FrameLayout { mSurfaceSynchronizer = synchronizer != null ? synchronizer : DEFAULT_SURFACE_SYNCHRONIZER; mBubbleContainer = new PhysicsAnimationLayout(context); - mBubbleContainer.setMaxRenderedChildren( - getResources().getInteger(R.integer.bubbles_max_rendered)); - mBubbleContainer.setController(mStackAnimationController); + mBubbleContainer.setActiveController(mStackAnimationController); mBubbleContainer.setElevation(elevation); mBubbleContainer.setClipChildren(false); addView(mBubbleContainer, new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)); @@ -728,7 +726,7 @@ public class BubbleStackView extends FrameLayout { public void updateBubbleOrder(List<Bubble> bubbles) { for (int i = 0; i < bubbles.size(); i++) { Bubble bubble = bubbles.get(i); - mBubbleContainer.moveViewTo(bubble.iconView, i); + mBubbleContainer.reorderView(bubble.iconView, i); } } @@ -908,19 +906,17 @@ public class BubbleStackView extends FrameLayout { }; if (shouldExpand) { - mBubbleContainer.setController(mExpandedAnimationController); - mExpandedAnimationController.expandFromStack( - mStackAnimationController.getStackPositionAlongNearestHorizontalEdge() - /* collapseTo */, - () -> { - updatePointerPosition(); - updateAfter.run(); - } /* after */); + mBubbleContainer.setActiveController(mExpandedAnimationController); + mExpandedAnimationController.expandFromStack(() -> { + updatePointerPosition(); + updateAfter.run(); + } /* after */); } else { mBubbleContainer.cancelAllAnimations(); mExpandedAnimationController.collapseBackToStack( + mStackAnimationController.getStackPositionAlongNearestHorizontalEdge(), () -> { - mBubbleContainer.setController(mStackAnimationController); + mBubbleContainer.setActiveController(mStackAnimationController); updateAfter.run(); }); } @@ -1013,7 +1009,7 @@ public class BubbleStackView extends FrameLayout { } mStackAnimationController.cancelStackPositionAnimations(); - mBubbleContainer.setController(mStackAnimationController); + mBubbleContainer.setActiveController(mStackAnimationController); hideFlyoutImmediate(); mDraggingInDismissTarget = false; @@ -1111,6 +1107,10 @@ public class BubbleStackView extends FrameLayout { /** Called when a gesture is completed or cancelled. */ void onGestureFinished() { mIsGestureInProgress = false; + + if (mIsExpanded) { + mExpandedAnimationController.onGestureFinished(); + } } /** Prepares and starts the desaturate/darken animation on the bubble stack. */ @@ -1201,6 +1201,7 @@ public class BubbleStackView extends FrameLayout { */ void magnetToStackIfNeededThenAnimateDismissal( View touchedView, float velX, float velY, Runnable after) { + final View draggedOutBubble = mExpandedAnimationController.getDraggedOutBubble(); final Runnable animateDismissal = () -> { mAfterMagnet = null; @@ -1218,7 +1219,7 @@ public class BubbleStackView extends FrameLayout { resetDesaturationAndDarken(); }); } else { - mExpandedAnimationController.dismissDraggedOutBubble(() -> { + mExpandedAnimationController.dismissDraggedOutBubble(draggedOutBubble, () -> { mAnimatingMagnet = false; mShowingDismiss = false; mDraggingInDismissTarget = false; @@ -1385,10 +1386,18 @@ public class BubbleStackView extends FrameLayout { }; // Post in case layout isn't complete and getWidth returns 0. - post(() -> mFlyout.showFlyout( - updateMessage, mStackAnimationController.getStackPosition(), getWidth(), - mStackAnimationController.isStackOnLeftSide(), - bubble.iconView.getBadgeColor(), mAfterFlyoutHides)); + post(() -> { + // An auto-expanding bubble could have been posted during the time it takes to + // layout. + if (isExpanded()) { + return; + } + + mFlyout.showFlyout( + updateMessage, mStackAnimationController.getStackPosition(), getWidth(), + mStackAnimationController.isStackOnLeftSide(), + bubble.iconView.getBadgeColor(), mAfterFlyoutHides); + }); } mFlyout.removeCallbacks(mHideFlyout); diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java b/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java index 24337a3c3312..1fa0e12452e1 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java @@ -22,6 +22,7 @@ import android.graphics.PointF; import android.view.View; import android.view.WindowInsets; +import androidx.annotation.Nullable; import androidx.dynamicanimation.animation.DynamicAnimation; import androidx.dynamicanimation.animation.SpringForce; @@ -63,12 +64,16 @@ public class ExpandedAnimationController private Point mDisplaySize; /** Size of dismiss target at bottom of screen. */ private float mPipDismissHeight; - /** Max number of bubbles shown in row above expanded view.*/ - private int mBubblesMaxRendered; /** Whether the dragged-out bubble is in the dismiss target. */ private boolean mIndividualBubbleWithinDismissTarget = false; + private boolean mAnimatingExpand = false; + private boolean mAnimatingCollapse = false; + private Runnable mAfterExpand; + private Runnable mAfterCollapse; + private PointF mCollapsePoint; + /** * Whether the dragged out bubble is springing towards the touch point, rather than using the * default behavior of moving directly to the touch point. @@ -97,56 +102,60 @@ public class ExpandedAnimationController private View mBubbleDraggingOut; /** - * Drag velocities for the dragging-out bubble when the drag finished. These are used by - * {@link #onChildRemoved} to animate out the bubble while respecting touch velocity. + * Animates expanding the bubbles into a row along the top of the screen. */ - private float mBubbleDraggingOutVelX; - private float mBubbleDraggingOutVelY; + public void expandFromStack(Runnable after) { + mAnimatingCollapse = false; + mAnimatingExpand = true; + mAfterExpand = after; - @Override - protected void setLayout(PhysicsAnimationLayout layout) { - super.setLayout(layout); + startOrUpdateExpandAnimation(); + } - final Resources res = layout.getResources(); - mStackOffsetPx = res.getDimensionPixelSize(R.dimen.bubble_stack_offset); - mBubblePaddingPx = res.getDimensionPixelSize(R.dimen.bubble_padding); - mBubbleSizePx = res.getDimensionPixelSize(R.dimen.individual_bubble_size); - mStatusBarHeight = - res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height); - mPipDismissHeight = res.getDimensionPixelSize(R.dimen.pip_dismiss_gradient_height); - mBubblesMaxRendered = res.getInteger(R.integer.bubbles_max_rendered); + /** Animate collapsing the bubbles back to their stacked position. */ + public void collapseBackToStack(PointF collapsePoint, Runnable after) { + mAnimatingExpand = false; + mAnimatingCollapse = true; + mAfterCollapse = after; + mCollapsePoint = collapsePoint; + + startOrUpdateCollapseAnimation(); } - /** - * Animates expanding the bubbles into a row along the top of the screen. - */ - public void expandFromStack(PointF collapseTo, Runnable after) { + private void startOrUpdateExpandAnimation() { animationsForChildrenFromIndex( 0, /* startIndex */ - new ChildAnimationConfigurator() { - @Override - public void configureAnimationForChildAtIndex( - int index, PhysicsAnimationLayout.PhysicsPropertyAnimator animation) { - animation.position(getBubbleLeft(index), getExpandedY()); + (index, animation) -> animation.position(getBubbleLeft(index), getExpandedY())) + .startAll(() -> { + mAnimatingExpand = false; + + if (mAfterExpand != null) { + mAfterExpand.run(); } - }) - .startAll(after); - mCollapseToPoint = collapseTo; + mAfterExpand = null; + }); } - /** Animate collapsing the bubbles back to their stacked position. */ - public void collapseBackToStack(Runnable after) { + private void startOrUpdateCollapseAnimation() { // Stack to the left if we're going to the left, or right if not. - final float sideMultiplier = mLayout.isFirstChildXLeftOfCenter(mCollapseToPoint.x) ? -1 : 1; - + final float sideMultiplier = mLayout.isFirstChildXLeftOfCenter(mCollapsePoint.x) ? -1 : 1; animationsForChildrenFromIndex( 0, /* startIndex */ - (index, animation) -> + (index, animation) -> { animation.position( - mCollapseToPoint.x + (sideMultiplier * index * mStackOffsetPx), - mCollapseToPoint.y)) - .startAll(after /* endAction */); + mCollapsePoint.x + (sideMultiplier * index * mStackOffsetPx), + mCollapsePoint.y); + }) + .startAll(() -> { + mAnimatingCollapse = false; + + if (mAfterCollapse != null) { + mAfterCollapse.run(); + } + + mAfterCollapse = null; + }); } /** Prepares the given bubble to be dragged out. */ @@ -190,10 +199,10 @@ public class ExpandedAnimationController } /** Plays a dismiss animation on the dragged out bubble. */ - public void dismissDraggedOutBubble(Runnable after) { + public void dismissDraggedOutBubble(View bubble, Runnable after) { mIndividualBubbleWithinDismissTarget = false; - animationForChild(mBubbleDraggingOut) + animationForChild(bubble) .withStiffness(SpringForce.STIFFNESS_HIGH) .scaleX(1.1f) .scaleY(1.1f) @@ -203,6 +212,10 @@ public class ExpandedAnimationController updateBubblePositions(); } + @Nullable public View getDraggedOutBubble() { + return mBubbleDraggingOut; + } + /** Magnets the given bubble to the dismiss target. */ public void magnetBubbleToDismiss( View bubbleView, float velX, float velY, float destY, Runnable after) { @@ -241,24 +254,17 @@ public class ExpandedAnimationController final int index = mLayout.indexOfChild(bubbleView); animationForChildAtIndex(index) - .position(getBubbleLeft(index), getExpandedY()) - .withPositionStartVelocities(velX, velY) - .start(() -> bubbleView.setTranslationZ(0f) /* after */); + .position(getBubbleLeft(index), getExpandedY()) + .withPositionStartVelocities(velX, velY) + .start(() -> bubbleView.setTranslationZ(0f) /* after */); - mBubbleDraggingOut = null; - mBubbleDraggedOutEnough = false; updateBubblePositions(); } - /** - * Sets configuration variables so that when the given bubble is removed, the animations are - * started with the given velocities. - */ - public void prepareForDismissalWithVelocity(View bubbleView, float velX, float velY) { - mBubbleDraggingOut = bubbleView; - mBubbleDraggingOutVelX = velX; - mBubbleDraggingOutVelY = velY; + /** Resets bubble drag out gesture flags. */ + public void onGestureFinished() { mBubbleDraggedOutEnough = false; + mBubbleDraggingOut = null; } /** @@ -297,6 +303,23 @@ public class ExpandedAnimationController } @Override + void onActiveControllerForLayout(PhysicsAnimationLayout layout) { + final Resources res = layout.getResources(); + mStackOffsetPx = res.getDimensionPixelSize(R.dimen.bubble_stack_offset); + mBubblePaddingPx = res.getDimensionPixelSize(R.dimen.bubble_padding); + mBubbleSizePx = res.getDimensionPixelSize(R.dimen.individual_bubble_size); + mStatusBarHeight = + res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height); + mPipDismissHeight = res.getDimensionPixelSize(R.dimen.pip_dismiss_gradient_height); + + // Ensure that all child views are at 1x scale, and visible, in case they were animating + // in. + mLayout.setVisibility(View.VISIBLE); + animationsForChildrenFromIndex(0 /* startIndex */, (index, animation) -> + animation.scaleX(1f).scaleY(1f).alpha(1f)).startAll(); + } + + @Override Set<DynamicAnimation.ViewProperty> getAnimatedProperties() { return Sets.newHashSet( DynamicAnimation.TRANSLATION_X, @@ -325,14 +348,21 @@ public class ExpandedAnimationController @Override void onChildAdded(View child, int index) { - child.setTranslationX(getXForChildAtIndex(index)); - - animationForChild(child) - .translationY( - getExpandedY() - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR, /* from */ - getExpandedY() /* to */) - .start(); - updateBubblePositions(); + // If a bubble is added while the expand/collapse animations are playing, update the + // animation to include the new bubble. + if (mAnimatingExpand) { + startOrUpdateExpandAnimation(); + } else if (mAnimatingCollapse) { + startOrUpdateCollapseAnimation(); + } else { + child.setTranslationX(getXForChildAtIndex(index)); + animationForChild(child) + .translationY( + getExpandedY() - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR, /* from */ + getExpandedY() /* to */) + .start(); + updateBubblePositions(); + } } @Override @@ -357,19 +387,15 @@ public class ExpandedAnimationController } @Override - protected void setChildVisibility(View child, int index, int visibility) { - if (visibility == View.VISIBLE) { - // Set alpha to 0 but then become visible immediately so the animation is visible. - child.setAlpha(0f); - child.setVisibility(View.VISIBLE); - } - - animationForChild(child) - .alpha(visibility == View.GONE ? 0f : 1f) - .start(() -> super.setChildVisibility(child, index, visibility) /* after */); + void onChildReordered(View child, int oldIndex, int newIndex) { + updateBubblePositions(); } private void updateBubblePositions() { + if (mAnimatingExpand || mAnimatingCollapse) { + return; + } + for (int i = 0; i < mLayout.getChildCount(); i++) { final View bubble = mLayout.getChildAt(i); @@ -378,6 +404,7 @@ public class ExpandedAnimationController if (bubble.equals(mBubbleDraggingOut)) { return; } + animationForChild(bubble) .translationX(getBubbleLeft(i)) .start(); @@ -403,10 +430,7 @@ public class ExpandedAnimationController return 0; } int bubbleCount = mLayout.getChildCount(); - if (bubbleCount > mBubblesMaxRendered) { - // Only shown bubbles are relevant for calculating position. - bubbleCount = mBubblesMaxRendered; - } + // Width calculations. double bubble = bubbleCount * mBubbleSizePx; float gap = (bubbleCount - 1) * mBubblePaddingPx; diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayout.java b/packages/SystemUI/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayout.java index 997d2c4627d8..3a3339249d5b 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayout.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayout.java @@ -17,10 +17,12 @@ package com.android.systemui.bubbles.animation; import android.content.Context; +import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; +import androidx.annotation.Nullable; import androidx.dynamicanimation.animation.DynamicAnimation; import androidx.dynamicanimation.animation.SpringAnimation; import androidx.dynamicanimation.animation.SpringForce; @@ -137,12 +139,33 @@ public class PhysicsAnimationLayout extends FrameLayout { */ abstract void onChildRemoved(View child, int index, Runnable finishRemoval); + /** Called when a child view has been reordered in the view hierachy. */ + abstract void onChildReordered(View child, int oldIndex, int newIndex); + + /** + * Called when the controller is set as the active animation controller for the given + * layout. Once active, the controller can start animations using the animator instances + * returned by {@link #animationForChild}. + * + * While all animations started by the previous controller will be cancelled, the new + * controller should not make any assumptions about the state of the layout or its children. + * Their translation, alpha, scale, etc. values may have been changed by the previous + * controller and should be reset here if relevant. + */ + abstract void onActiveControllerForLayout(PhysicsAnimationLayout layout); + protected PhysicsAnimationLayout mLayout; PhysicsAnimationController() { } + /** Whether this controller is the currently active controller for its associated layout. */ + protected boolean isActiveController() { + return this == mLayout.mController; + } + protected void setLayout(PhysicsAnimationLayout layout) { this.mLayout = layout; + onActiveControllerForLayout(layout); } protected PhysicsAnimationLayout getLayout() { @@ -150,15 +173,6 @@ public class PhysicsAnimationLayout extends FrameLayout { } /** - * Sets the child's visibility when it moves beyond or within the limits set by a call to - * {@link PhysicsAnimationLayout#setMaxRenderedChildren}. This can be overridden to animate - * this transition. - */ - protected void setChildVisibility(View child, int index, int visibility) { - child.setVisibility(visibility); - } - - /** * Returns a {@link PhysicsPropertyAnimator} instance for the given child view. */ protected PhysicsPropertyAnimator animationForChild(View child) { @@ -170,6 +184,9 @@ public class PhysicsAnimationLayout extends FrameLayout { child.setTag(R.id.physics_animator_tag, animator); } + animator.clearAnimator(); + animator.setAssociatedController(this); + return animator; } @@ -235,32 +252,17 @@ public class PhysicsAnimationLayout extends FrameLayout { new HashMap<>(); /** The currently active animation controller. */ - private PhysicsAnimationController mController; - - /** - * The maximum number of children to render and animate at a time. See - * {@link #setMaxRenderedChildren}. - */ - private int mMaxRenderedChildren = 5; + @Nullable protected PhysicsAnimationController mController; public PhysicsAnimationLayout(Context context) { super(context); } /** - * The maximum number of children to render and animate at a time. Any child views added beyond - * this limit will be set to {@link View#GONE}. If any animations attempt to run on the view, - * the corresponding property will be set with no animation. - */ - public void setMaxRenderedChildren(int max) { - this.mMaxRenderedChildren = max; - } - - /** * Sets the animation controller and constructs or reconfigures the layout's physics animations * to meet the controller's specifications. */ - public void setController(PhysicsAnimationController controller) { + public void setActiveController(PhysicsAnimationController controller) { cancelAllAnimations(); mEndActionForProperty.clear(); @@ -312,42 +314,11 @@ public class PhysicsAnimationLayout extends FrameLayout { @Override public void addView(View child, int index, ViewGroup.LayoutParams params) { - super.addView(child, index, params); - - // Set up animations for the new view, if the controller is set. If it isn't set, we'll be - // setting up animations for all children when setController is called. - if (mController != null) { - for (DynamicAnimation.ViewProperty property : mController.getAnimatedProperties()) { - setUpAnimationForChild(property, child, index); - } - - mController.onChildAdded(child, index); - } - - setChildrenVisibility(); + addViewInternal(child, index, params, false /* isReorder */); } @Override public void removeView(View view) { - removeViewAndThen(view, /* callback */ null); - } - - @Override - public void removeViewAt(int index) { - removeView(getChildAt(index)); - } - - /** Immediately moves the view from wherever it currently is, to the given index. */ - public void moveViewTo(View view, int index) { - super.removeView(view); - addView(view, index); - } - - /** - * Let the controller know that this view should be removed, and then call the callback once the - * controller has finished any removal animations and the view has actually been removed. - */ - public void removeViewAndThen(View view, Runnable callback) { if (mController != null) { final int index = indexOfChild(view); @@ -355,8 +326,6 @@ public class PhysicsAnimationLayout extends FrameLayout { super.removeView(view); addTransientView(view, index); - setChildrenVisibility(); - // Tell the controller to animate this view out, and call the callback when it's // finished. mController.onChildRemoved(view, index, () -> { @@ -364,19 +333,28 @@ public class PhysicsAnimationLayout extends FrameLayout { // any are still running and then remove it. cancelAnimationsOnView(view); removeTransientView(view); - - if (callback != null) { - callback.run(); - } }); } else { // Without a controller, nobody will animate this view out, so it gets an unceremonious // departure. super.removeView(view); + } + } - if (callback != null) { - callback.run(); - } + @Override + public void removeViewAt(int index) { + removeView(getChildAt(index)); + } + + /** Immediately re-orders the view to the given index. */ + public void reorderView(View view, int index) { + final int oldIndex = indexOfChild(view); + + super.removeView(view); + addViewInternal(view, index, view.getLayoutParams(), true /* isReorder */); + + if (mController != null) { + mController.onChildReordered(view, oldIndex, index); } } @@ -427,6 +405,10 @@ public class PhysicsAnimationLayout extends FrameLayout { } } + protected boolean isActiveController(PhysicsAnimationController controller) { + return mController == controller; + } + /** Whether the first child would be left of center if translated to the given x value. */ protected boolean isFirstChildXLeftOfCenter(float x) { if (getChildCount() > 0) { @@ -454,6 +436,26 @@ public class PhysicsAnimationLayout extends FrameLayout { } /** + * Adds a view to the layout. If this addition is not the result of a call to + * {@link #reorderView}, this will also notify the controller via + * {@link PhysicsAnimationController#onChildAdded} and set up animations for the view. + */ + private void addViewInternal( + View child, int index, ViewGroup.LayoutParams params, boolean isReorder) { + super.addView(child, index, params); + + // Set up animations for the new view, if the controller is set. If it isn't set, we'll be + // setting up animations for all children when setActiveController is called. + if (mController != null && !isReorder) { + for (DynamicAnimation.ViewProperty property : mController.getAnimatedProperties()) { + setUpAnimationForChild(property, child, index); + } + + mController.onChildAdded(child, index); + } + } + + /** * Retrieves the animation of the given property from the view at the given index via the view * tag system. */ @@ -481,33 +483,16 @@ public class PhysicsAnimationLayout extends FrameLayout { SpringAnimation newAnim = new SpringAnimation(child, property); newAnim.addUpdateListener((animation, value, velocity) -> { final int indexOfChild = indexOfChild(child); - final int nextAnimInChain = - mController.getNextAnimationInChain(property, indexOfChild); + final int nextAnimInChain = mController.getNextAnimationInChain(property, indexOfChild); if (nextAnimInChain == PhysicsAnimationController.NONE || indexOfChild < 0) { return; } - final int animIndex = indexOfChild(child); - final float offset = - mController.getOffsetForChainedPropertyAnimation(property); - - // If this property's animations should be chained, then check to see if there is a - // subsequent animation within the rendering limit, and if so, tell it to animate to - // this animation's new value (plus the offset). - if (nextAnimInChain < Math.min(getChildCount(), mMaxRenderedChildren)) { - getAnimationAtIndex(property, animIndex + 1) + final float offset = mController.getOffsetForChainedPropertyAnimation(property); + if (nextAnimInChain < getChildCount()) { + getAnimationAtIndex(property, nextAnimInChain) .animateToFinalPosition(value + offset); - } else if (nextAnimInChain < getChildCount()) { - // If the next child view is not rendered, update the property directly without - // animating it, so that the view is still in the correct state if it later - // becomes visible. - for (int i = nextAnimInChain; i < getChildCount(); i++) { - // 'value' here is the value of the last child within the rendering limit, - // not the first child's value - so we want to subtract the last child's - // index when calculating the offset. - property.setValue(getChildAt(i), value + offset * (i - animIndex)); - } } }); @@ -516,22 +501,6 @@ public class PhysicsAnimationLayout extends FrameLayout { child.setTag(getTagIdForProperty(property), newAnim); } - /** Hides children beyond the max rendering count. */ - private void setChildrenVisibility() { - for (int i = 0; i < getChildCount(); i++) { - final int targetVisibility = i < mMaxRenderedChildren ? View.VISIBLE : View.GONE; - final View targetView = getChildAt(i); - - if (targetView.getVisibility() != targetVisibility) { - if (mController != null) { - mController.setChildVisibility(targetView, i, targetVisibility); - } else { - targetView.setVisibility(targetVisibility); - } - } - } - } - /** Return a stable ID to use as a tag key for the given property's animations. */ private int getTagIdForProperty(DynamicAnimation.ViewProperty property) { if (property.equals(DynamicAnimation.TRANSLATION_X)) { @@ -592,7 +561,7 @@ public class PhysicsAnimationLayout extends FrameLayout { private View mView; /** Start velocity to use for all property animations. */ - private float mDefaultStartVelocity = 0f; + private float mDefaultStartVelocity = -Float.MAX_VALUE; /** Start delay to use when start is called. */ private long mStartDelay = 0; @@ -625,6 +594,15 @@ public class PhysicsAnimationLayout extends FrameLayout { */ private Map<DynamicAnimation.ViewProperty, Float> mAnimatedProperties = new HashMap<>(); + /** + * All of the initial property values that have been set. These values will be instantly set + * when {@link #start} is called, just before the animation begins. + */ + private Map<DynamicAnimation.ViewProperty, Float> mInitialPropertyValues = new HashMap<>(); + + /** The animation controller that last retrieved this animator instance. */ + private PhysicsAnimationController mAssociatedController; + protected PhysicsPropertyAnimator(View view) { this.mView = view; } @@ -644,7 +622,7 @@ public class PhysicsAnimationLayout extends FrameLayout { /** Set the view's alpha value to 'from', then animate it to the given value. */ public PhysicsPropertyAnimator alpha(float from, float to, Runnable... endActions) { - mView.setAlpha(from); + mInitialPropertyValues.put(DynamicAnimation.ALPHA, from); return alpha(to, endActions); } @@ -656,7 +634,7 @@ public class PhysicsAnimationLayout extends FrameLayout { /** Set the view's translationX value to 'from', then animate it to the given value. */ public PhysicsPropertyAnimator translationX( float from, float to, Runnable... endActions) { - mView.setTranslationX(from); + mInitialPropertyValues.put(DynamicAnimation.TRANSLATION_X, from); return translationX(to, endActions); } @@ -668,7 +646,7 @@ public class PhysicsAnimationLayout extends FrameLayout { /** Set the view's translationY value to 'from', then animate it to the given value. */ public PhysicsPropertyAnimator translationY( float from, float to, Runnable... endActions) { - mView.setTranslationY(from); + mInitialPropertyValues.put(DynamicAnimation.TRANSLATION_Y, from); return translationY(to, endActions); } @@ -690,7 +668,7 @@ public class PhysicsAnimationLayout extends FrameLayout { /** Set the view's scaleX value to 'from', then animate it to the given value. */ public PhysicsPropertyAnimator scaleX(float from, float to, Runnable... endActions) { - mView.setScaleX(from); + mInitialPropertyValues.put(DynamicAnimation.SCALE_X, from); return scaleX(to, endActions); } @@ -701,7 +679,7 @@ public class PhysicsAnimationLayout extends FrameLayout { /** Set the view's scaleY value to 'from', then animate it to the given value. */ public PhysicsPropertyAnimator scaleY(float from, float to, Runnable... endActions) { - mView.setScaleY(from); + mInitialPropertyValues.put(DynamicAnimation.SCALE_Y, from); return scaleY(to, endActions); } @@ -750,6 +728,13 @@ public class PhysicsAnimationLayout extends FrameLayout { * animated property on every child (including chained animations) have ended. */ public void start(Runnable... after) { + if (!isActiveController(mAssociatedController)) { + Log.w(TAG, "Only the active animation controller is allowed to start animations. " + + "Use PhysicsAnimationLayout#setActiveController to set the active " + + "animation controller."); + return; + } + final Set<DynamicAnimation.ViewProperty> properties = getAnimatedProperties(); // If there are end actions, set an end listener on the layout for all the properties @@ -791,6 +776,10 @@ public class PhysicsAnimationLayout extends FrameLayout { // Actually start the animations. for (DynamicAnimation.ViewProperty property : properties) { + if (mInitialPropertyValues.containsKey(property)) { + property.setValue(mView, mInitialPropertyValues.get(property)); + } + final SpringForce defaultSpringForce = mController.getSpringForce(property, mView); animateValueForChild( property, @@ -803,14 +792,7 @@ public class PhysicsAnimationLayout extends FrameLayout { mEndActionsForProperty.get(property)); } - // Clear out the animator. - mAnimatedProperties.clear(); - mPositionStartVelocities.clear(); - mDefaultStartVelocity = 0; - mStartDelay = 0; - mStiffness = -1; - mDampingRatio = -1; - mEndActionsForProperty.clear(); + clearAnimator(); } /** Returns the set of properties that will animate once {@link #start} is called. */ @@ -847,20 +829,50 @@ public class PhysicsAnimationLayout extends FrameLayout { }); } - animation.getSpring().setStiffness(stiffness); - animation.getSpring().setDampingRatio(dampingRatio); + final SpringForce animationSpring = animation.getSpring(); - if (startVel > 0) { - animation.setStartVelocity(startVel); + if (animationSpring == null) { + return; } + final Runnable configureAndStartAnimation = () -> { + animationSpring.setStiffness(stiffness); + animationSpring.setDampingRatio(dampingRatio); + + if (startVel > -Float.MAX_VALUE) { + animation.setStartVelocity(startVel); + } + + animationSpring.setFinalPosition(value); + animation.start(); + }; + if (startDelay > 0) { - postDelayed(() -> animation.animateToFinalPosition(value), startDelay); + postDelayed(configureAndStartAnimation, startDelay); } else { - animation.animateToFinalPosition(value); + configureAndStartAnimation.run(); } } } + + private void clearAnimator() { + mInitialPropertyValues.clear(); + mAnimatedProperties.clear(); + mPositionStartVelocities.clear(); + mDefaultStartVelocity = -Float.MAX_VALUE; + mStartDelay = 0; + mStiffness = -1; + mDampingRatio = -1; + mEndActionsForProperty.clear(); + } + + /** + * Sets the controller that last retrieved this animator instance, so that we can prevent + * {@link #start} from actually starting animations if called by a non-active controller. + */ + private void setAssociatedController(PhysicsAnimationController controller) { + mAssociatedController = controller; + } } @Override diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java b/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java index b9cdc844eef9..ab8752e4195f 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java @@ -154,21 +154,6 @@ public class StackAnimationController extends /** Height of the status bar. */ private float mStatusBarHeight; - @Override - protected void setLayout(PhysicsAnimationLayout layout) { - super.setLayout(layout); - - Resources res = layout.getResources(); - mStackOffset = res.getDimensionPixelSize(R.dimen.bubble_stack_offset); - mIndividualBubbleSize = res.getDimensionPixelSize(R.dimen.individual_bubble_size); - mBubblePadding = res.getDimensionPixelSize(R.dimen.bubble_padding); - mBubbleOffscreen = res.getDimensionPixelSize(R.dimen.bubble_stack_offscreen); - mStackStartingVerticalOffset = - res.getDimensionPixelSize(R.dimen.bubble_stack_starting_offset_y); - mStatusBarHeight = - res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height); - } - /** * Instantly move the first bubble to the given point, and animate the rest of the stack behind * it with the 'following' effect. @@ -286,6 +271,8 @@ public class StackAnimationController extends }, DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y); + // If we're flinging now, there's no more touch event to catch up to. + mFirstBubbleSpringingToTouch = false; mIsMovingFromFlinging = true; return destinationRelativeX; } @@ -656,19 +643,38 @@ public class StackAnimationController extends if (mLayout.getChildCount() > 0) { animationForChildAtIndex(0).translationX(mStackPosition.x).start(); + } else { + // Set the start position back to the default since we're out of bubbles. New bubbles + // will then animate in from the start position. + mStackPosition = getDefaultStartPosition(); } } + @Override + void onChildReordered(View child, int oldIndex, int newIndex) {} + + @Override + void onActiveControllerForLayout(PhysicsAnimationLayout layout) { + Resources res = layout.getResources(); + mStackOffset = res.getDimensionPixelSize(R.dimen.bubble_stack_offset); + mIndividualBubbleSize = res.getDimensionPixelSize(R.dimen.individual_bubble_size); + mBubblePadding = res.getDimensionPixelSize(R.dimen.bubble_padding); + mBubbleOffscreen = res.getDimensionPixelSize(R.dimen.bubble_stack_offscreen); + mStackStartingVerticalOffset = + res.getDimensionPixelSize(R.dimen.bubble_stack_starting_offset_y); + mStatusBarHeight = + res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height); + } + /** Moves the stack, without any animation, to the starting position. */ private void moveStackToStartPosition() { // Post to ensure that the layout's width and height have been calculated. mLayout.setVisibility(View.INVISIBLE); mLayout.post(() -> { + setStackPosition(mRestingStackPosition == null + ? getDefaultStartPosition() + : mRestingStackPosition); mStackMovedToStartPosition = true; - setStackPosition( - mRestingStackPosition == null - ? getDefaultStartPosition() - : mRestingStackPosition); mLayout.setVisibility(View.VISIBLE); // Animate in the top bubble now that we're visible. @@ -707,15 +713,20 @@ public class StackAnimationController extends Log.d(TAG, String.format("Setting position to (%f, %f).", pos.x, pos.y)); mStackPosition.set(pos.x, pos.y); - mLayout.cancelAllAnimations(); - cancelStackPositionAnimations(); - - // Since we're not using the chained animations, apply the offsets manually. - final float xOffset = getOffsetForChainedPropertyAnimation(DynamicAnimation.TRANSLATION_X); - final float yOffset = getOffsetForChainedPropertyAnimation(DynamicAnimation.TRANSLATION_Y); - for (int i = 0; i < mLayout.getChildCount(); i++) { - mLayout.getChildAt(i).setTranslationX(pos.x + (i * xOffset)); - mLayout.getChildAt(i).setTranslationY(pos.y + (i * yOffset)); + // If we're not the active controller, we don't want to physically move the bubble views. + if (isActiveController()) { + mLayout.cancelAllAnimations(); + cancelStackPositionAnimations(); + + // Since we're not using the chained animations, apply the offsets manually. + final float xOffset = getOffsetForChainedPropertyAnimation( + DynamicAnimation.TRANSLATION_X); + final float yOffset = getOffsetForChainedPropertyAnimation( + DynamicAnimation.TRANSLATION_Y); + for (int i = 0; i < mLayout.getChildCount(); i++) { + mLayout.getChildAt(i).setTranslationX(pos.x + (i * xOffset)); + mLayout.getChildAt(i).setTranslationY(pos.y + (i * yOffset)); + } } } @@ -732,6 +743,10 @@ public class StackAnimationController extends /** Animates in the given bubble. */ private void animateInBubble(View child) { + if (!isActiveController()) { + return; + } + child.setTranslationY(mStackPosition.y); float xOffset = getOffsetForChainedPropertyAnimation(DynamicAnimation.TRANSLATION_X); diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java index ff16ed0f1477..86472008688f 100644 --- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java +++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java @@ -1588,17 +1588,13 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, // Disable rotation suggestions, if enabled setRotationSuggestionsEnabled(false); - FrameLayout panelContainer = new FrameLayout(mContext); + FrameLayout panelContainer = + findViewById(com.android.systemui.R.id.global_actions_panel_container); FrameLayout.LayoutParams panelParams = new FrameLayout.LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT, - FrameLayout.LayoutParams.WRAP_CONTENT); + FrameLayout.LayoutParams.MATCH_PARENT); panelContainer.addView(mPanelController.getPanelContent(), panelParams); - addContentView( - panelContainer, - new ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT)); mBackgroundDrawable = mPanelController.getBackgroundDrawable(); mScrimAlpha = 1f; } @@ -1606,8 +1602,10 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, private void initializeLayout() { setContentView(getGlobalActionsLayoutId(mContext)); + fixNavBarClipping(); mGlobalActionsLayout = findViewById(com.android.systemui.R.id.global_actions_view); mGlobalActionsLayout.setOutsideTouchListener(view -> dismiss()); + ((View) mGlobalActionsLayout.getParent()).setOnClickListener(view -> dismiss()); mGlobalActionsLayout.setListViewAccessibilityDelegate(new View.AccessibilityDelegate() { @Override public boolean dispatchPopulateAccessibilityEvent( @@ -1630,6 +1628,15 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, getWindow().setBackgroundDrawable(mBackgroundDrawable); } + private void fixNavBarClipping() { + ViewGroup content = findViewById(android.R.id.content); + content.setClipChildren(false); + content.setClipToPadding(false); + ViewGroup contentParent = (ViewGroup) content.getParent(); + contentParent.setClipChildren(false); + contentParent.setClipToPadding(false); + } + private int getGlobalActionsLayoutId(Context context) { int rotation = RotationUtils.getRotation(context); boolean useGridLayout = isForceGridEnabled(context) diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsGridLayout.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsGridLayout.java index 03165f47c472..e1462d15c887 100644 --- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsGridLayout.java +++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsGridLayout.java @@ -42,8 +42,6 @@ public class GlobalActionsGridLayout extends GlobalActionsLayout { listView.setReverseSublists(shouldReverseSublists()); listView.setReverseItems(shouldReverseListItems()); listView.setSwapRowsAndColumns(shouldSwapRowsAndColumns()); - - fixNavBarClipping(); } @Override @@ -75,19 +73,6 @@ public class GlobalActionsGridLayout extends GlobalActionsLayout { } } - /** - * Allows the dialog to clip over the navbar, which prevents shadows and animations from being - * cut off. - */ - private void fixNavBarClipping() { - ViewGroup parent = (ViewGroup) this.getParent(); - ViewGroup parentParent = (ViewGroup) parent.getParent(); - parent.setClipChildren(false); - parent.setClipToPadding(false); - parentParent.setClipChildren(false); - parentParent.setClipToPadding(false); - } - @Override protected ListGridLayout getListView() { return (ListGridLayout) super.getListView(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java index b7a154d67c10..a0cda693822f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java @@ -22,8 +22,6 @@ import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Rect; -import android.graphics.Region; -import android.graphics.Region.Op; import android.util.Log; import android.util.Pools; import android.view.DisplayCutout; @@ -78,6 +76,7 @@ public class HeadsUpManagerPhone extends HeadsUpManager implements Dumpable, private int[] mTmpTwoArray = new int[2]; private boolean mHeadsUpGoingAway; private int mStatusBarState; + private Rect mTouchableRegion = new Rect(); private AnimationStateHandler mAnimationStateHandler; @@ -297,10 +296,13 @@ public class HeadsUpManagerPhone extends HeadsUpManager implements Dumpable, @Nullable public void updateTouchableRegion(ViewTreeObserver.InternalInsetsInfo info) { info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); + info.touchableRegion.set(calculateTouchableRegion()); + } + public Rect calculateTouchableRegion() { if (!hasPinnedHeadsUp()) { - info.touchableRegion.set(0, 0, mStatusBarWindowView.getWidth(), mStatusBarHeight); - updateRegionForNotch(info.touchableRegion); + mTouchableRegion.set(0, 0, mStatusBarWindowView.getWidth(), mStatusBarHeight); + updateRegionForNotch(mTouchableRegion); } else { NotificationEntry topEntry = getTopEntry(); if (topEntry.isChildInGroup()) { @@ -315,11 +317,12 @@ public class HeadsUpManagerPhone extends HeadsUpManager implements Dumpable, int minX = mTmpTwoArray[0]; int maxX = mTmpTwoArray[0] + topRow.getWidth(); int height = topRow.getIntrinsicHeight(); - info.touchableRegion.set(minX, 0, maxX, mHeadsUpInset + height); + mTouchableRegion.set(minX, 0, maxX, mHeadsUpInset + height); } + return mTouchableRegion; } - private void updateRegionForNotch(Region region) { + private void updateRegionForNotch(Rect region) { DisplayCutout cutout = mStatusBarWindowView.getRootWindowInsets().getDisplayCutout(); if (cutout == null) { return; @@ -330,7 +333,7 @@ public class HeadsUpManagerPhone extends HeadsUpManager implements Dumpable, Rect bounds = new Rect(); ScreenDecorations.DisplayCutoutView.boundsFromDirection(cutout, Gravity.TOP, bounds); bounds.offset(0, mDisplayCutoutTouchableRegionSize); - region.op(bounds, Op.UNION); + region.union(bounds); } @Override 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 7623dee10c39..92aa884b14d2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java @@ -100,6 +100,7 @@ import com.android.systemui.util.InjectionInflationController; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.function.Consumer; @@ -141,6 +142,7 @@ public class NotificationPanelView extends PanelView implements private static final String COUNTER_PANEL_OPEN_PEEK = "panel_open_peek"; private static final Rect mDummyDirtyRect = new Rect(0, 0, 1, 1); + private static final Rect mEmptyRect = new Rect(); private static final AnimationProperties CLOCK_ANIMATION_PROPERTIES = new AnimationProperties() .setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD); @@ -596,6 +598,25 @@ public class NotificationPanelView extends PanelView implements mQs.setHeightOverride(mQs.getDesiredHeight()); } updateMaxHeadsUpTranslation(); + updateGestureExclusionRect(); + } + + private void updateGestureExclusionRect() { + Rect exclusionRect = calculateGestureExclusionRect(); + setSystemGestureExclusionRects(exclusionRect.isEmpty() + ? Collections.EMPTY_LIST + : Collections.singletonList(exclusionRect)); + } + + private Rect calculateGestureExclusionRect() { + Rect exclusionRect = null; + if (isFullyCollapsed()) { + // Note: The heads up manager also calculates the non-pinned touchable region + exclusionRect = mHeadsUpManager.calculateTouchableRegion(); + } + return exclusionRect != null + ? exclusionRect + : mEmptyRect; } private void setIsFullWidth(boolean isFullWidth) { @@ -1798,6 +1819,7 @@ public class NotificationPanelView extends PanelView implements updateHeader(); updateNotificationTranslucency(); updatePanelExpanded(); + updateGestureExclusionRect(); if (DEBUG) { invalidate(); } @@ -2568,6 +2590,7 @@ public class NotificationPanelView extends PanelView implements mNotificationStackScroller.runAfterAnimationFinished( mHeadsUpExistenceChangedRunnable); } + updateGestureExclusionRect(); } public void setHeadsUpAnimatingAway(boolean headsUpAnimatingAway) { @@ -2992,6 +3015,7 @@ public class NotificationPanelView extends PanelView implements @Override public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { super.dump(fd, pw, args); + pw.println(" gestureExclusionRect: " + calculateGestureExclusionRect()); if (mKeyguardStatusBar != null) { mKeyguardStatusBar.dump(fd, pw, args); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/ExpandedAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/ExpandedAnimationControllerTest.java index 756cf3e138f6..b324235106c2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/ExpandedAnimationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/ExpandedAnimationControllerTest.java @@ -60,8 +60,8 @@ public class ExpandedAnimationControllerTest extends PhysicsAnimationLayoutTestC @Before public void setUp() throws Exception { super.setUp(); - addOneMoreThanRenderLimitBubbles(); - mLayout.setController(mExpandedController); + addOneMoreThanBubbleLimitBubbles(); + mLayout.setActiveController(mExpandedController); Resources res = mLayout.getResources(); mStackOffset = res.getDimensionPixelSize(R.dimen.bubble_stack_offset); @@ -73,14 +73,14 @@ public class ExpandedAnimationControllerTest extends PhysicsAnimationLayoutTestC @Test public void testExpansionAndCollapse() throws InterruptedException { Runnable afterExpand = Mockito.mock(Runnable.class); - mExpandedController.expandFromStack(mExpansionPoint, afterExpand); + mExpandedController.expandFromStack(afterExpand); waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y); testBubblesInCorrectExpandedPositions(); verify(afterExpand).run(); Runnable afterCollapse = Mockito.mock(Runnable.class); - mExpandedController.collapseBackToStack(afterCollapse); + mExpandedController.collapseBackToStack(mExpansionPoint, afterCollapse); waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y); testStackedAtPosition(mExpansionPoint.x, mExpansionPoint.y, -1); @@ -139,7 +139,6 @@ public class ExpandedAnimationControllerTest extends PhysicsAnimationLayoutTestC assertEquals(500f, draggedBubble.getTranslationY(), 1f); // Snap it back and make sure it made it back correctly. - mExpandedController.prepareForDismissalWithVelocity(draggedBubble, 0f, 0f); mLayout.removeView(draggedBubble); waitForLayoutMessageQueue(); waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y); @@ -169,7 +168,7 @@ public class ExpandedAnimationControllerTest extends PhysicsAnimationLayoutTestC // Dismiss the now-magneted bubble, verify that the callback was called. final Runnable afterDismiss = Mockito.mock(Runnable.class); - mExpandedController.dismissDraggedOutBubble(afterDismiss); + mExpandedController.dismissDraggedOutBubble(draggedOutView, afterDismiss); waitForPropertyAnimations(DynamicAnimation.ALPHA); verify(after).run(); @@ -224,7 +223,7 @@ public class ExpandedAnimationControllerTest extends PhysicsAnimationLayoutTestC /** Expand the stack and wait for animations to finish. */ private void expand() throws InterruptedException { - mExpandedController.expandFromStack(mExpansionPoint, Mockito.mock(Runnable.class)); + mExpandedController.expandFromStack(Mockito.mock(Runnable.class)); waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y); } @@ -236,26 +235,19 @@ public class ExpandedAnimationControllerTest extends PhysicsAnimationLayoutTestC assertEquals(x + i * offsetMultiplier * mStackOffset, mLayout.getChildAt(i).getTranslationX(), 2f); assertEquals(y, mLayout.getChildAt(i).getTranslationY(), 2f); - - if (i < mMaxRenderedBubbles) { - assertEquals(1f, mLayout.getChildAt(i).getAlpha(), .01f); - } + assertEquals(1f, mLayout.getChildAt(i).getAlpha(), .01f); } } /** Check that children are in the correct positions for being expanded. */ private void testBubblesInCorrectExpandedPositions() { // Check all the visible bubbles to see if they're in the right place. - for (int i = 0; i < Math.min(mLayout.getChildCount(), mMaxRenderedBubbles); i++) { + for (int i = 0; i < mLayout.getChildCount(); i++) { assertEquals(getBubbleLeft(i), mLayout.getChildAt(i).getTranslationX(), 2f); assertEquals(mExpandedController.getExpandedY(), mLayout.getChildAt(i).getTranslationY(), 2f); - - if (i < mMaxRenderedBubbles) { - assertEquals(1f, mLayout.getChildAt(i).getAlpha(), .01f); - } } } @@ -273,9 +265,7 @@ public class ExpandedAnimationControllerTest extends PhysicsAnimationLayoutTestC return 0; } int bubbleCount = mLayout.getChildCount(); - if (bubbleCount > mMaxRenderedBubbles) { - bubbleCount = mMaxRenderedBubbles; - } + // Width calculations. double bubble = bubbleCount * mBubbleSize; float gap = (bubbleCount - 1) * mBubblePadding; diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayoutTest.java index eef6ddcf6d6b..f8b32c213109 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayoutTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayoutTest.java @@ -23,7 +23,6 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.inOrder; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -77,21 +76,9 @@ public class PhysicsAnimationLayoutTest extends PhysicsAnimationLayoutTestCase { } @Test - public void testRenderVisibility() throws InterruptedException { - mLayout.setController(mTestableController); - addOneMoreThanRenderLimitBubbles(); - - // The last child should be GONE, the rest VISIBLE. - for (int i = 0; i < mMaxRenderedBubbles + 1; i++) { - assertEquals(i == mMaxRenderedBubbles ? View.GONE : View.VISIBLE, - mLayout.getChildAt(i).getVisibility()); - } - } - - @Test public void testHierarchyChanges() throws InterruptedException { - mLayout.setController(mTestableController); - addOneMoreThanRenderLimitBubbles(); + mLayout.setActiveController(mTestableController); + addOneMoreThanBubbleLimitBubbles(); // Make sure the controller was notified of all the views we added. for (View mView : mViews) { @@ -115,8 +102,8 @@ public class PhysicsAnimationLayoutTest extends PhysicsAnimationLayoutTestCase { @Test public void testUpdateValueNotChained() throws InterruptedException { - mLayout.setController(mTestableController); - addOneMoreThanRenderLimitBubbles(); + mLayout.setActiveController(mTestableController); + addOneMoreThanBubbleLimitBubbles(); // Don't chain any values. mTestableController.setChainedProperties(Sets.newHashSet()); @@ -146,8 +133,8 @@ public class PhysicsAnimationLayoutTest extends PhysicsAnimationLayoutTestCase { @Test public void testSetEndActions() throws InterruptedException { - mLayout.setController(mTestableController); - addOneMoreThanRenderLimitBubbles(); + mLayout.setActiveController(mTestableController); + addOneMoreThanBubbleLimitBubbles(); mTestableController.setChainedProperties(Sets.newHashSet()); final CountDownLatch xLatch = new CountDownLatch(1); @@ -189,8 +176,8 @@ public class PhysicsAnimationLayoutTest extends PhysicsAnimationLayoutTestCase { @Test public void testRemoveEndListeners() throws InterruptedException { - mLayout.setController(mTestableController); - addOneMoreThanRenderLimitBubbles(); + mLayout.setActiveController(mTestableController); + addOneMoreThanBubbleLimitBubbles(); mTestableController.setChainedProperties(Sets.newHashSet()); final CountDownLatch xLatch = new CountDownLatch(1); @@ -229,8 +216,8 @@ public class PhysicsAnimationLayoutTest extends PhysicsAnimationLayoutTestCase { public void testSetController() throws InterruptedException { // Add the bubbles, then set the controller, to make sure that a controller added to an // already-initialized view works correctly. - addOneMoreThanRenderLimitBubbles(); - mLayout.setController(mTestableController); + addOneMoreThanBubbleLimitBubbles(); + mLayout.setActiveController(mTestableController); testChainedTranslationAnimations(); TestableAnimationController secondController = @@ -243,7 +230,7 @@ public class PhysicsAnimationLayoutTest extends PhysicsAnimationLayoutTestCase { DynamicAnimation.SCALE_X, 10f); secondController.setRemoveImmediately(true); - mLayout.setController(secondController); + mLayout.setActiveController(secondController); mTestableController.animationForChildAtIndex(0) .scaleX(1.5f) .start(); @@ -266,7 +253,7 @@ public class PhysicsAnimationLayoutTest extends PhysicsAnimationLayoutTestCase { Mockito.verify(secondController, Mockito.atLeastOnce()) .getOffsetForChainedPropertyAnimation(eq(DynamicAnimation.SCALE_X)); - mLayout.setController(mTestableController); + mLayout.setActiveController(mTestableController); mTestableController.animationForChildAtIndex(0) .translationX(100f) .start(); @@ -283,8 +270,8 @@ public class PhysicsAnimationLayoutTest extends PhysicsAnimationLayoutTestCase { @Test public void testArePropertiesAnimating() throws InterruptedException { - mLayout.setController(mTestableController); - addOneMoreThanRenderLimitBubbles(); + mLayout.setActiveController(mTestableController); + addOneMoreThanBubbleLimitBubbles(); assertFalse(mLayout.arePropertiesAnimating( DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y)); @@ -307,8 +294,8 @@ public class PhysicsAnimationLayoutTest extends PhysicsAnimationLayoutTestCase { @Test public void testCancelAllAnimations() throws InterruptedException { - mLayout.setController(mTestableController); - addOneMoreThanRenderLimitBubbles(); + mLayout.setActiveController(mTestableController); + addOneMoreThanBubbleLimitBubbles(); mTestableController.animationForChildAtIndex(0) .position(1000, 1000) @@ -321,29 +308,10 @@ public class PhysicsAnimationLayoutTest extends PhysicsAnimationLayoutTestCase { assertTrue(mViews.get(0).getTranslationY() < 1000); } - @Test - public void testSetChildVisibility() throws InterruptedException { - mLayout.setController(mTestableController); - addOneMoreThanRenderLimitBubbles(); - - // The last view should have been set to GONE by the controller, since we added one more - // than the limit and it got pushed off. None of the first children should have been set - // VISIBLE, since they would have been animated in by onChildAdded. - Mockito.verify(mTestableController).setChildVisibility( - mViews.get(mViews.size() - 1), 5, View.GONE); - Mockito.verify(mTestableController, never()).setChildVisibility( - any(View.class), anyInt(), eq(View.VISIBLE)); - - // Remove the first view, which should cause the last view to become visible again. - mLayout.removeView(mViews.get(0)); - Mockito.verify(mTestableController).setChildVisibility( - mViews.get(mViews.size() - 1), 4, View.VISIBLE); - } - /** Standard test of chained translation animations. */ private void testChainedTranslationAnimations() throws InterruptedException { - mLayout.setController(mTestableController); - addOneMoreThanRenderLimitBubbles(); + mLayout.setActiveController(mTestableController); + addOneMoreThanBubbleLimitBubbles(); assertEquals(0, mLayout.getChildAt(0).getTranslationX(), .1f); assertEquals(0, mLayout.getChildAt(1).getTranslationX(), .1f); @@ -354,11 +322,7 @@ public class PhysicsAnimationLayoutTest extends PhysicsAnimationLayoutTestCase { waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X); - // Since we enabled chaining, animating the first view to 100 should animate the second to - // 115 (since we set the offset to 15) and the third to 130, etc. Despite the sixth bubble - // not being visible, or animated, make sure that it has the appropriate chained - // translation. - for (int i = 0; i < mMaxRenderedBubbles + 1; i++) { + for (int i = 0; i < mLayout.getChildCount(); i++) { assertEquals( 100 + i * TEST_TRANSLATION_X_OFFSET, mLayout.getChildAt(i).getTranslationX(), .1f); @@ -383,8 +347,8 @@ public class PhysicsAnimationLayoutTest extends PhysicsAnimationLayoutTestCase { @Test public void testPhysicsAnimator() throws InterruptedException { - mLayout.setController(mTestableController); - addOneMoreThanRenderLimitBubbles(); + mLayout.setActiveController(mTestableController); + addOneMoreThanBubbleLimitBubbles(); Runnable afterAll = Mockito.mock(Runnable.class); Runnable after = Mockito.spy(new Runnable() { @@ -430,9 +394,9 @@ public class PhysicsAnimationLayoutTest extends PhysicsAnimationLayoutTestCase { // Don't chain since we're going to invoke each animation independently. mTestableController.setChainedProperties(new HashSet<>()); - mLayout.setController(mTestableController); + mLayout.setActiveController(mTestableController); - addOneMoreThanRenderLimitBubbles(); + addOneMoreThanBubbleLimitBubbles(); Runnable allEnd = Mockito.mock(Runnable.class); @@ -452,7 +416,7 @@ public class PhysicsAnimationLayoutTest extends PhysicsAnimationLayoutTestCase { @Test public void testAnimationsForChildrenFromIndex_noChildren() { - mLayout.setController(mTestableController); + mLayout.setActiveController(mTestableController); final Runnable after = Mockito.mock(Runnable.class); mTestableController @@ -523,8 +487,9 @@ public class PhysicsAnimationLayoutTest extends PhysicsAnimationLayoutTestCase { } @Override - protected void setChildVisibility(View child, int index, int visibility) { - super.setChildVisibility(child, index, visibility); - } + void onChildReordered(View child, int oldIndex, int newIndex) {} + + @Override + void onActiveControllerForLayout(PhysicsAnimationLayout layout) {} } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayoutTestCase.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayoutTestCase.java index c6acef5d4907..f633f3996d13 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayoutTestCase.java +++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayoutTestCase.java @@ -56,7 +56,6 @@ public class PhysicsAnimationLayoutTestCase extends SysuiTestCase { Handler mMainThreadHandler; - int mMaxRenderedBubbles; int mSystemWindowInsetSize = 50; int mCutoutInsetSize = 100; @@ -69,6 +68,8 @@ public class PhysicsAnimationLayoutTestCase extends SysuiTestCase { @Mock private DisplayCutout mCutout; + private int mMaxBubbles; + @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); @@ -79,7 +80,7 @@ public class PhysicsAnimationLayoutTestCase extends SysuiTestCase { mLayout.setTop(0); mLayout.setBottom(mHeight); - mMaxRenderedBubbles = + mMaxBubbles = getContext().getResources().getInteger(R.integer.bubbles_max_rendered); mMainThreadHandler = new Handler(Looper.getMainLooper()); @@ -96,8 +97,8 @@ public class PhysicsAnimationLayoutTestCase extends SysuiTestCase { } /** Add one extra bubble over the limit, so we can make sure it's gone/chains appropriately. */ - void addOneMoreThanRenderLimitBubbles() throws InterruptedException { - for (int i = 0; i < mMaxRenderedBubbles + 1; i++) { + void addOneMoreThanBubbleLimitBubbles() throws InterruptedException { + for (int i = 0; i < mMaxBubbles + 1; i++) { final View newView = new FrameLayout(mContext); mLayout.addView(newView, 0); mViews.add(0, newView); @@ -138,6 +139,13 @@ public class PhysicsAnimationLayoutTestCase extends SysuiTestCase { } @Override + protected boolean isActiveController(PhysicsAnimationController controller) { + // Return true since otherwise all test controllers will be seen as inactive since they + // are wrapped by MainThreadAnimationControllerWrapper. + return true; + } + + @Override public boolean post(Runnable action) { return mMainThreadHandler.post(action); } @@ -148,9 +156,9 @@ public class PhysicsAnimationLayoutTestCase extends SysuiTestCase { } @Override - public void setController(PhysicsAnimationController controller) { + public void setActiveController(PhysicsAnimationController controller) { runOnMainThreadAndBlock( - () -> super.setController( + () -> super.setActiveController( new MainThreadAnimationControllerWrapper(controller))); } @@ -267,8 +275,15 @@ public class PhysicsAnimationLayoutTestCase extends SysuiTestCase { } @Override - protected void setChildVisibility(View child, int index, int visibility) { - mWrappedController.setChildVisibility(child, index, visibility); + void onChildReordered(View child, int oldIndex, int newIndex) { + runOnMainThreadAndBlock( + () -> mWrappedController.onChildReordered(child, oldIndex, newIndex)); + } + + @Override + void onActiveControllerForLayout(PhysicsAnimationLayout layout) { + runOnMainThreadAndBlock( + () -> mWrappedController.onActiveControllerForLayout(layout)); } @Override diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/StackAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/StackAnimationControllerTest.java index 9218a8b93f66..31a7d5a45b68 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/StackAnimationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/StackAnimationControllerTest.java @@ -54,8 +54,8 @@ public class StackAnimationControllerTest extends PhysicsAnimationLayoutTestCase @Before public void setUp() throws Exception { super.setUp(); - mLayout.setController(mStackController); - addOneMoreThanRenderLimitBubbles(); + mLayout.setActiveController(mStackController); + addOneMoreThanBubbleLimitBubbles(); mStackOffset = mLayout.getResources().getDimensionPixelSize(R.dimen.bubble_stack_offset); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsGridLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsGridLayoutTest.java index a396f3e8bf46..ea478597ef77 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsGridLayoutTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsGridLayoutTest.java @@ -48,8 +48,9 @@ public class GlobalActionsGridLayoutTest extends SysuiTestCase { @Before public void setUp() throws Exception { - mGridLayout = spy((GlobalActionsGridLayout) - LayoutInflater.from(mContext).inflate(R.layout.global_actions_grid, null)); + mGridLayout = spy(LayoutInflater.from(mContext) + .inflate(R.layout.global_actions_grid, null) + .requireViewById(R.id.global_actions_view)); mListGrid = spy(mGridLayout.getListView()); doReturn(mListGrid).when(mGridLayout).getListView(); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/globalactions/ListGridLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/globalactions/ListGridLayoutTest.java index 746140fc725d..74e8cc280979 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/globalactions/ListGridLayoutTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/globalactions/ListGridLayoutTest.java @@ -43,8 +43,9 @@ public class ListGridLayoutTest extends SysuiTestCase { @Before public void setUp() throws Exception { - GlobalActionsGridLayout globalActions = (GlobalActionsGridLayout) - LayoutInflater.from(mContext).inflate(R.layout.global_actions_grid, null); + GlobalActionsGridLayout globalActions = LayoutInflater.from(mContext) + .inflate(R.layout.global_actions_grid, null) + .requireViewById(R.id.global_actions_view); mListGridLayout = globalActions.getListView(); } diff --git a/services/core/java/com/android/server/LocationManagerService.java b/services/core/java/com/android/server/LocationManagerService.java index d52fe8170358..ae04f76b95b4 100644 --- a/services/core/java/com/android/server/LocationManagerService.java +++ b/services/core/java/com/android/server/LocationManagerService.java @@ -82,6 +82,7 @@ import android.os.UserManager; import android.os.WorkSource; import android.os.WorkSource.WorkChain; import android.provider.Settings; +import android.stats.location.LocationStatsEnums; import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; @@ -269,10 +270,14 @@ public class LocationManagerService extends ILocationManager.Stub { @PowerManager.LocationPowerSaveMode private int mBatterySaverMode; + @GuardedBy("mLock") + private final LocationUsageLogger mLocationUsageLogger; + public LocationManagerService(Context context) { super(); mContext = context; mHandler = FgThread.getHandler(); + mLocationUsageLogger = new LocationUsageLogger(); // Let the package manager query which are the default location // providers as they get certain permissions granted by default. @@ -2346,7 +2351,18 @@ public class LocationManagerService extends ILocationManager.Stub { * Method to be called when a record will no longer be used. */ private void disposeLocked(boolean removeReceiver) { - mRequestStatistics.stopRequesting(mReceiver.mCallerIdentity.mPackageName, mProvider); + String packageName = mReceiver.mCallerIdentity.mPackageName; + mRequestStatistics.stopRequesting(packageName, mProvider); + + mLocationUsageLogger.logLocationApiUsage( + LocationStatsEnums.USAGE_ENDED, + LocationStatsEnums.API_REQUEST_LOCATION_UPDATES, + packageName, + mRealRequest, + mReceiver.isListener(), + mReceiver.isPendingIntent(), + /* radius= */ 0, + mActivityManager.getPackageImportance(packageName)); // remove from mRecordsByProvider ArrayList<UpdateRecord> globalRecords = mRecordsByProvider.get(this.mProvider); @@ -2521,6 +2537,13 @@ public class LocationManagerService extends ILocationManager.Stub { "cannot register both listener and intent"); } + mLocationUsageLogger.logLocationApiUsage( + LocationStatsEnums.USAGE_STARTED, + LocationStatsEnums.API_REQUEST_LOCATION_UPDATES, + packageName, request, listener != null, intent != null, + /* radius= */ 0, + mActivityManager.getPackageImportance(packageName)); + Receiver receiver; if (intent != null) { receiver = getReceiverLocked(intent, pid, uid, packageName, workSource, @@ -2813,6 +2836,18 @@ public class LocationManagerService extends ILocationManager.Stub { } long identity = Binder.clearCallingIdentity(); try { + synchronized (mLock) { + mLocationUsageLogger.logLocationApiUsage( + LocationStatsEnums.USAGE_STARTED, + LocationStatsEnums.API_REQUEST_GEOFENCE, + packageName, + request, + /* hasListener= */ false, + intent != null, + geofence.getRadius(), + mActivityManager.getPackageImportance(packageName)); + } + mGeofenceManager.addFence(sanitizedRequest, geofence, intent, allowedResolutionLevel, uid, packageName); @@ -2833,6 +2868,17 @@ public class LocationManagerService extends ILocationManager.Stub { // geo-fence manager uses the public location API, need to clear identity long identity = Binder.clearCallingIdentity(); try { + synchronized (mLock) { + mLocationUsageLogger.logLocationApiUsage( + LocationStatsEnums.USAGE_ENDED, + LocationStatsEnums.API_REQUEST_GEOFENCE, + packageName, + /* LocationRequest= */ null, + /* hasListener= */ false, + intent != null, + geofence.getRadius(), + mActivityManager.getPackageImportance(packageName)); + } mGeofenceManager.removeFence(geofence, intent); } finally { Binder.restoreCallingIdentity(identity); @@ -2916,6 +2962,20 @@ public class LocationManagerService extends ILocationManager.Stub { gnssDataListeners.put(binder, linkedListener); long identity = Binder.clearCallingIdentity(); try { + if (gnssDataProvider == mGnssNavigationMessageProvider + || gnssDataProvider == mGnssStatusProvider) { + mLocationUsageLogger.logLocationApiUsage( + LocationStatsEnums.USAGE_STARTED, + gnssDataProvider == mGnssNavigationMessageProvider + ? LocationStatsEnums.API_ADD_GNSS_MEASUREMENTS_LISTENER + : LocationStatsEnums.API_REGISTER_GNSS_STATUS_CALLBACK, + packageName, + /* LocationRequest= */ null, + /* hasListener= */ true, + /* hasIntent= */ false, + /* radius */ 0, + mActivityManager.getPackageImportance(packageName)); + } if (isThrottlingExemptLocked(callerIdentity) || isImportanceForeground( mActivityManager.getPackageImportance(packageName))) { @@ -2941,6 +3001,26 @@ public class LocationManagerService extends ILocationManager.Stub { if (linkedListener == null) { return; } + long identity = Binder.clearCallingIdentity(); + try { + if (gnssDataProvider == mGnssNavigationMessageProvider + || gnssDataProvider == mGnssStatusProvider) { + mLocationUsageLogger.logLocationApiUsage( + LocationStatsEnums.USAGE_ENDED, + gnssDataProvider == mGnssNavigationMessageProvider + ? LocationStatsEnums.API_ADD_GNSS_MEASUREMENTS_LISTENER + : LocationStatsEnums.API_REGISTER_GNSS_STATUS_CALLBACK, + linkedListener.mCallerIdentity.mPackageName, + /* LocationRequest= */ null, + /* hasListener= */ true, + /* hasIntent= */ false, + /* radius= */ 0, + mActivityManager.getPackageImportance( + linkedListener.mCallerIdentity.mPackageName)); + } + } finally { + Binder.restoreCallingIdentity(identity); + } unlinkFromListenerDeathNotificationLocked(binder, linkedListener); gnssDataProvider.removeListener(listener); } @@ -3026,6 +3106,11 @@ public class LocationManagerService extends ILocationManager.Stub { checkResolutionLevelIsSufficientForProviderUseLocked(getCallerAllowedResolutionLevel(), providerName); + mLocationUsageLogger.logLocationApiUsage( + LocationStatsEnums.USAGE_STARTED, + LocationStatsEnums.API_SEND_EXTRA_COMMAND, + providerName); + // and check for ACCESS_LOCATION_EXTRA_COMMANDS if ((mContext.checkCallingOrSelfPermission(ACCESS_LOCATION_EXTRA_COMMANDS) != PERMISSION_GRANTED)) { @@ -3037,6 +3122,11 @@ public class LocationManagerService extends ILocationManager.Stub { provider.sendExtraCommandLocked(command, extras); } + mLocationUsageLogger.logLocationApiUsage( + LocationStatsEnums.USAGE_ENDED, + LocationStatsEnums.API_SEND_EXTRA_COMMAND, + providerName); + return true; } } diff --git a/services/core/java/com/android/server/LocationUsageLogger.java b/services/core/java/com/android/server/LocationUsageLogger.java new file mode 100644 index 000000000000..c5030351f69d --- /dev/null +++ b/services/core/java/com/android/server/LocationUsageLogger.java @@ -0,0 +1,265 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server; + +import android.app.ActivityManager; +import android.location.LocationManager; +import android.location.LocationRequest; +import android.os.SystemClock; +import android.stats.location.LocationStatsEnums; +import android.util.Log; +import android.util.StatsLog; + +import java.time.Instant; + +/** + * Logger for Location API usage logging. + */ +class LocationUsageLogger { + private static final String TAG = "LocationUsageLogger"; + private static final boolean D = Log.isLoggable(TAG, Log.DEBUG); + + private static final int ONE_SEC_IN_MILLIS = 1000; + private static final int ONE_MINUTE_IN_MILLIS = 60000; + private static final int ONE_HOUR_IN_MILLIS = 3600000; + + private long mLastApiUsageLogHour = 0; + + private int mApiUsageLogHourlyCount = 0; + + private static final int API_USAGE_LOG_HOURLY_CAP = 60; + + private static int providerNameToStatsdEnum(String provider) { + if (LocationManager.NETWORK_PROVIDER.equals(provider)) { + return LocationStatsEnums.PROVIDER_NETWORK; + } else if (LocationManager.GPS_PROVIDER.equals(provider)) { + return LocationStatsEnums.PROVIDER_GPS; + } else if (LocationManager.PASSIVE_PROVIDER.equals(provider)) { + return LocationStatsEnums.PROVIDER_PASSIVE; + } else if (LocationManager.FUSED_PROVIDER.equals(provider)) { + return LocationStatsEnums.PROVIDER_FUSED; + } else { + return LocationStatsEnums.PROVIDER_UNKNOWN; + } + } + + private static int bucketizeIntervalToStatsdEnum(long interval) { + // LocationManager already converts negative values to 0. + if (interval < ONE_SEC_IN_MILLIS) { + return LocationStatsEnums.INTERVAL_BETWEEN_0_SEC_AND_1_SEC; + } else if (interval < ONE_SEC_IN_MILLIS * 5) { + return LocationStatsEnums.INTERVAL_BETWEEN_1_SEC_AND_5_SEC; + } else if (interval < ONE_MINUTE_IN_MILLIS) { + return LocationStatsEnums.INTERVAL_BETWEEN_5_SEC_AND_1_MIN; + } else if (interval < ONE_MINUTE_IN_MILLIS * 10) { + return LocationStatsEnums.INTERVAL_BETWEEN_1_MIN_AND_10_MIN; + } else if (interval < ONE_HOUR_IN_MILLIS) { + return LocationStatsEnums.INTERVAL_BETWEEN_10_MIN_AND_1_HOUR; + } else { + return LocationStatsEnums.INTERVAL_LARGER_THAN_1_HOUR; + } + } + + private static int bucketizeSmallestDisplacementToStatsdEnum(float smallestDisplacement) { + // LocationManager already converts negative values to 0. + if (smallestDisplacement == 0) { + return LocationStatsEnums.DISTANCE_ZERO; + } else if (smallestDisplacement > 0 && smallestDisplacement <= 100) { + return LocationStatsEnums.DISTANCE_BETWEEN_0_AND_100; + } else { + return LocationStatsEnums.DISTANCE_LARGER_THAN_100; + } + } + + private static int bucketizeRadiusToStatsdEnum(float radius) { + if (radius < 0) { + return LocationStatsEnums.RADIUS_NEGATIVE; + } else if (radius < 100) { + return LocationStatsEnums.RADIUS_BETWEEN_0_AND_100; + } else if (radius < 200) { + return LocationStatsEnums.RADIUS_BETWEEN_100_AND_200; + } else if (radius < 300) { + return LocationStatsEnums.RADIUS_BETWEEN_200_AND_300; + } else if (radius < 1000) { + return LocationStatsEnums.RADIUS_BETWEEN_300_AND_1000; + } else if (radius < 10000) { + return LocationStatsEnums.RADIUS_BETWEEN_1000_AND_10000; + } else { + return LocationStatsEnums.RADIUS_LARGER_THAN_100000; + } + } + + private static int getBucketizedExpireIn(long expireAt) { + if (expireAt == Long.MAX_VALUE) { + return LocationStatsEnums.EXPIRATION_NO_EXPIRY; + } + + long elapsedRealtime = SystemClock.elapsedRealtime(); + long expireIn = Math.max(0, expireAt - elapsedRealtime); + + if (expireIn < 20 * ONE_SEC_IN_MILLIS) { + return LocationStatsEnums.EXPIRATION_BETWEEN_0_AND_20_SEC; + } else if (expireIn < ONE_MINUTE_IN_MILLIS) { + return LocationStatsEnums.EXPIRATION_BETWEEN_20_SEC_AND_1_MIN; + } else if (expireIn < ONE_MINUTE_IN_MILLIS * 10) { + return LocationStatsEnums.EXPIRATION_BETWEEN_1_MIN_AND_10_MIN; + } else if (expireIn < ONE_HOUR_IN_MILLIS) { + return LocationStatsEnums.EXPIRATION_BETWEEN_10_MIN_AND_1_HOUR; + } else { + return LocationStatsEnums.EXPIRATION_LARGER_THAN_1_HOUR; + } + } + + private static int categorizeActivityImportance(int importance) { + if (importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) { + return LocationStatsEnums.IMPORTANCE_TOP; + } else if (importance == ActivityManager + .RunningAppProcessInfo + .IMPORTANCE_FOREGROUND_SERVICE) { + return LocationStatsEnums.IMPORTANCE_FORGROUND_SERVICE; + } else { + return LocationStatsEnums.IMPORTANCE_BACKGROUND; + } + } + + private static int getCallbackType( + int apiType, boolean hasListener, boolean hasIntent) { + if (apiType == LocationStatsEnums.API_SEND_EXTRA_COMMAND) { + return LocationStatsEnums.CALLBACK_NOT_APPLICABLE; + } + + // Listener and PendingIntent will not be set at + // the same time. + if (hasIntent) { + return LocationStatsEnums.CALLBACK_PENDING_INTENT; + } else if (hasListener) { + return LocationStatsEnums.CALLBACK_LISTENER; + } else { + return LocationStatsEnums.CALLBACK_UNKNOWN; + } + } + + // Update the hourly count of APIUsage log event. + // Returns false if hit the hourly log cap. + private boolean checkApiUsageLogCap() { + if (D) { + Log.d(TAG, "checking APIUsage log cap."); + } + + long currentHour = Instant.now().toEpochMilli() / ONE_HOUR_IN_MILLIS; + if (currentHour > mLastApiUsageLogHour) { + mLastApiUsageLogHour = currentHour; + mApiUsageLogHourlyCount = 0; + return true; + } else { + mApiUsageLogHourlyCount = Math.min( + mApiUsageLogHourlyCount + 1, API_USAGE_LOG_HOURLY_CAP); + return mApiUsageLogHourlyCount < API_USAGE_LOG_HOURLY_CAP; + } + } + + /** + * Log a Location API usage event to Statsd. + * Logging event is capped at 60 per hour. Usage events exceeding + * the cap will be dropped by LocationUsageLogger. + */ + public void logLocationApiUsage(int usageType, int apiInUse, + String packageName, LocationRequest locationRequest, + boolean hasListener, boolean hasIntent, + float radius, int activityImportance) { + try { + if (!checkApiUsageLogCap()) { + return; + } + + boolean isLocationRequestNull = locationRequest == null; + if (D) { + Log.d(TAG, "log API Usage to statsd. usageType: " + usageType + ", apiInUse: " + + apiInUse + ", packageName: " + (packageName == null ? "" : packageName) + + ", locationRequest: " + + (isLocationRequestNull ? "" : locationRequest.toString()) + + ", hasListener: " + hasListener + + ", hasIntent: " + hasIntent + + ", radius: " + radius + + ", importance: " + activityImportance); + } + + StatsLog.write(StatsLog.LOCATION_MANAGER_API_USAGE_REPORTED, usageType, + apiInUse, packageName, + isLocationRequestNull + ? LocationStatsEnums.PROVIDER_UNKNOWN + : providerNameToStatsdEnum(locationRequest.getProvider()), + isLocationRequestNull + ? LocationStatsEnums.QUALITY_UNKNOWN + : locationRequest.getQuality(), + isLocationRequestNull + ? LocationStatsEnums.INTERVAL_UNKNOWN + : bucketizeIntervalToStatsdEnum(locationRequest.getInterval()), + isLocationRequestNull + ? LocationStatsEnums.DISTANCE_UNKNOWN + : bucketizeSmallestDisplacementToStatsdEnum( + locationRequest.getSmallestDisplacement()), + isLocationRequestNull ? 0 : locationRequest.getNumUpdates(), + // only log expireIn for USAGE_STARTED + isLocationRequestNull || usageType == LocationStatsEnums.USAGE_ENDED + ? LocationStatsEnums.EXPIRATION_UNKNOWN + : getBucketizedExpireIn(locationRequest.getExpireAt()), + getCallbackType(apiInUse, hasListener, hasIntent), + bucketizeRadiusToStatsdEnum(radius), + categorizeActivityImportance(activityImportance)); + } catch (Exception e) { + // Swallow exceptions to avoid crashing LMS. + Log.w(TAG, "Failed to log API usage to statsd.", e); + } + } + + /** + * Log a Location API usage event to Statsd. + * Logging event is capped at 60 per hour. Usage events exceeding + * the cap will be dropped by LocationUsageLogger. + */ + public void logLocationApiUsage(int usageType, int apiInUse, String providerName) { + try { + if (!checkApiUsageLogCap()) { + return; + } + + if (D) { + Log.d(TAG, "log API Usage to statsd. usageType: " + usageType + ", apiInUse: " + + apiInUse + ", providerName: " + providerName); + } + + StatsLog.write(StatsLog.LOCATION_MANAGER_API_USAGE_REPORTED, usageType, apiInUse, + /* package_name= */ null, + providerNameToStatsdEnum(providerName), + LocationStatsEnums.QUALITY_UNKNOWN, + LocationStatsEnums.INTERVAL_UNKNOWN, + LocationStatsEnums.DISTANCE_UNKNOWN, + /* numUpdates= */ 0, + LocationStatsEnums.EXPIRATION_UNKNOWN, + getCallbackType( + apiInUse, + /* isListenerNull= */ true, + /* isIntentNull= */ true), + /* bucketizedRadius= */ 0, + LocationStatsEnums.IMPORTANCE_UNKNOWN); + } catch (Exception e) { + // Swallow exceptions to avoid crashing LMS. + Log.w(TAG, "Failed to log API usage to statsd.", e); + } + } +} diff --git a/services/core/java/com/android/server/PackageWatchdog.java b/services/core/java/com/android/server/PackageWatchdog.java index 32671144aee3..dee89e5d5c4d 100644 --- a/services/core/java/com/android/server/PackageWatchdog.java +++ b/services/core/java/com/android/server/PackageWatchdog.java @@ -25,6 +25,7 @@ import android.annotation.Nullable; import android.content.Context; import android.content.pm.PackageManager; import android.content.pm.VersionedPackage; +import android.net.NetworkStackClient; import android.os.Environment; import android.os.Handler; import android.os.Looper; @@ -115,6 +116,7 @@ public class PackageWatchdog { // File containing the XML data of monitored packages /data/system/package-watchdog.xml private final AtomicFile mPolicyFile; private final ExplicitHealthCheckController mHealthCheckController; + private final NetworkStackClient mNetworkStackClient; @GuardedBy("mLock") private boolean mIsPackagesReady; // Flag to control whether explicit health checks are supported or not @@ -135,7 +137,8 @@ public class PackageWatchdog { new File(new File(Environment.getDataDirectory(), "system"), "package-watchdog.xml")), new Handler(Looper.myLooper()), BackgroundThread.getHandler(), - new ExplicitHealthCheckController(context)); + new ExplicitHealthCheckController(context), + NetworkStackClient.getInstance()); } /** @@ -143,12 +146,14 @@ public class PackageWatchdog { */ @VisibleForTesting PackageWatchdog(Context context, AtomicFile policyFile, Handler shortTaskHandler, - Handler longTaskHandler, ExplicitHealthCheckController controller) { + Handler longTaskHandler, ExplicitHealthCheckController controller, + NetworkStackClient networkStackClient) { mContext = context; mPolicyFile = policyFile; mShortTaskHandler = shortTaskHandler; mLongTaskHandler = longTaskHandler; mHealthCheckController = controller; + mNetworkStackClient = networkStackClient; loadFromFile(); } @@ -174,6 +179,7 @@ public class PackageWatchdog { () -> syncRequestsAsync()); setPropertyChangedListenerLocked(); updateConfigs(); + registerNetworkStackHealthListener(); } } @@ -630,29 +636,40 @@ public class PackageWatchdog { synchronized (mLock) { PackageHealthObserver registeredObserver = observer.mRegisteredObserver; if (registeredObserver != null) { - PackageManager pm = mContext.getPackageManager(); Iterator<MonitoredPackage> it = failedPackages.iterator(); while (it.hasNext()) { String failedPackage = it.next().getName(); - long versionCode = 0; Slog.i(TAG, "Explicit health check failed for package " + failedPackage); - try { - versionCode = pm.getPackageInfo( - failedPackage, 0 /* flags */).getLongVersionCode(); - } catch (PackageManager.NameNotFoundException e) { + VersionedPackage versionedPkg = getVersionedPackage(failedPackage); + if (versionedPkg == null) { Slog.w(TAG, "Explicit health check failed but could not find package " + failedPackage); // TODO(b/120598832): Skip. We only continue to pass tests for now since // the tests don't install any packages + versionedPkg = new VersionedPackage(failedPackage, 0L); } - registeredObserver.execute( - new VersionedPackage(failedPackage, versionCode)); + registeredObserver.execute(versionedPkg); } } } }); } + @Nullable + private VersionedPackage getVersionedPackage(String packageName) { + final PackageManager pm = mContext.getPackageManager(); + if (pm == null) { + return null; + } + try { + final long versionCode = pm.getPackageInfo( + packageName, 0 /* flags */).getLongVersionCode(); + return new VersionedPackage(packageName, versionCode); + } catch (PackageManager.NameNotFoundException e) { + return null; + } + } + /** * Loads mAllObservers from file. * @@ -726,6 +743,27 @@ public class PackageWatchdog { } } + private void registerNetworkStackHealthListener() { + // TODO: have an internal method to trigger a rollback by reporting high severity errors, + // and rely on ActivityManager to inform the watchdog of severe network stack crashes + // instead of having this listener in parallel. + mNetworkStackClient.registerHealthListener( + packageName -> { + final VersionedPackage pkg = getVersionedPackage(packageName); + if (pkg == null) { + Slog.wtf(TAG, "NetworkStack failed but could not find its package"); + return; + } + // This is a severe failure and recovery should be attempted immediately. + // TODO: have a better way to handle such failures. + final List<VersionedPackage> pkgList = Collections.singletonList(pkg); + final long failureCount = getTriggerFailureCount(); + for (int i = 0; i < failureCount; i++) { + onPackageFailure(pkgList); + } + }); + } + /** * Persists mAllObservers to file. Threshold information is ignored. */ diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java index a0522e3971e7..fcd6a0aacd92 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java +++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java @@ -241,15 +241,6 @@ import com.android.internal.annotations.GuardedBy; sendLMsgNoDelay(MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT, SENDMSG_QUEUE, info); } - /*package*/ void postBluetoothA2dpDeviceConfigChangeExt( - @NonNull BluetoothDevice device, - @AudioService.BtProfileConnectionState int state, int profile, - boolean suppressNoisyIntent, int a2dpVolume) { - final BtDeviceConnectionInfo info = new BtDeviceConnectionInfo(device, state, profile, - suppressNoisyIntent, a2dpVolume); - sendLMsgNoDelay(MSG_L_A2DP_ACTIVE_DEVICE_CHANGE_EXT, SENDMSG_QUEUE, info); - } - private static final class HearingAidDeviceConnectionInfo { final @NonNull BluetoothDevice mDevice; final @AudioService.BtProfileConnectionState int mState; @@ -862,22 +853,6 @@ import com.android.internal.annotations.GuardedBy; info.mDevice, info.mState, info.mSupprNoisy, info.mMusicDevice); } } break; - case MSG_L_A2DP_ACTIVE_DEVICE_CHANGE_EXT: { - final BtDeviceConnectionInfo info = (BtDeviceConnectionInfo) msg.obj; - AudioService.sDeviceLogger.log((new AudioEventLogger.StringEvent( - "handleBluetoothA2dpActiveDeviceChangeExt " - + " state=" + info.mState - // only querying address as this is the only readily available - // field on the device - + " addr=" + info.mDevice.getAddress() - + " prof=" + info.mProfile + " supprNoisy=" + info.mSupprNoisy - + " vol=" + info.mVolume)).printLog(TAG)); - synchronized (mDeviceStateLock) { - mDeviceInventory.handleBluetoothA2dpActiveDeviceChangeExt( - info.mDevice, info.mState, info.mProfile, - info.mSupprNoisy, info.mVolume); - } - } break; default: Log.wtf(TAG, "Invalid message " + msg.what); } @@ -925,10 +900,8 @@ import com.android.internal.annotations.GuardedBy; private static final int MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT = 27; // process external command to (dis)connect a hearing aid device private static final int MSG_L_HEARING_AID_DEVICE_CONNECTION_CHANGE_EXT = 28; - // process external command to (dis)connect or change active A2DP device - private static final int MSG_L_A2DP_ACTIVE_DEVICE_CHANGE_EXT = 29; // a ScoClient died in BtHelper - private static final int MSG_L_SCOCLIENT_DIED = 30; + private static final int MSG_L_SCOCLIENT_DIED = 29; private static boolean isMessageHandledUnderWakelock(int msgId) { @@ -943,7 +916,6 @@ import com.android.internal.annotations.GuardedBy; case MSG_L_A2DP_ACTIVE_DEVICE_CHANGE: case MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT: case MSG_L_HEARING_AID_DEVICE_CONNECTION_CHANGE_EXT: - case MSG_L_A2DP_ACTIVE_DEVICE_CHANGE_EXT: return true; default: return false; diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java index 887c90873bdd..99b97cbf7dbc 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java +++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java @@ -570,49 +570,6 @@ public final class AudioDeviceInventory { } } - /*package*/ void handleBluetoothA2dpActiveDeviceChangeExt( - @NonNull BluetoothDevice device, - @AudioService.BtProfileConnectionState int state, int profile, - boolean suppressNoisyIntent, int a2dpVolume) { - if (state == BluetoothProfile.STATE_DISCONNECTED) { - mDeviceBroker.postBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent( - device, state, profile, suppressNoisyIntent, a2dpVolume); - return; - } - // state == BluetoothProfile.STATE_CONNECTED - synchronized (mConnectedDevices) { - final String address = device.getAddress(); - final int a2dpCodec = mDeviceBroker.getA2dpCodec(device); - final String deviceKey = DeviceInfo.makeDeviceListKey( - AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, address); - DeviceInfo deviceInfo = mConnectedDevices.get(deviceKey); - if (deviceInfo != null) { - // Device config change for matching A2DP device - mDeviceBroker.postBluetoothA2dpDeviceConfigChange(device); - return; - } - for (int i = 0; i < mConnectedDevices.size(); i++) { - deviceInfo = mConnectedDevices.valueAt(i); - if (deviceInfo.mDeviceType != AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP) { - continue; - } - // A2DP device exists, handle active device change - final String existingDevicekey = mConnectedDevices.keyAt(i); - mConnectedDevices.remove(existingDevicekey); - mConnectedDevices.put(deviceKey, new DeviceInfo( - AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, BtHelper.getName(device), - address, a2dpCodec)); - mDeviceBroker.postA2dpActiveDeviceChange( - new BtHelper.BluetoothA2dpDeviceInfo( - device, a2dpVolume, a2dpCodec)); - return; - } - } - // New A2DP device connection - mDeviceBroker.postBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent( - device, state, profile, suppressNoisyIntent, a2dpVolume); - } - /*package*/ int setWiredDeviceConnectionState(int type, @AudioService.ConnectionState int state, String address, String name, String caller) { synchronized (mConnectedDevices) { diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 15e8851a6a2b..30035c8c365b 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -4382,27 +4382,6 @@ public class AudioService extends IAudioService.Stub mDeviceBroker.postBluetoothA2dpDeviceConfigChange(device); } - /** - * @see AudioManager#handleBluetoothA2dpActiveDeviceChange(BluetoothDevice, int, int, - * boolean, int) - */ - public void handleBluetoothA2dpActiveDeviceChange( - BluetoothDevice device, int state, int profile, boolean suppressNoisyIntent, - int a2dpVolume) { - if (device == null) { - throw new IllegalArgumentException("Illegal null device"); - } - if (profile != BluetoothProfile.A2DP && profile != BluetoothProfile.A2DP_SINK) { - throw new IllegalArgumentException("invalid profile " + profile); - } - if (state != BluetoothProfile.STATE_CONNECTED - && state != BluetoothProfile.STATE_DISCONNECTED) { - throw new IllegalArgumentException("Invalid state " + state); - } - mDeviceBroker.postBluetoothA2dpDeviceConfigChangeExt(device, state, profile, - suppressNoisyIntent, a2dpVolume); - } - private static final int DEVICE_MEDIA_UNMUTED_ON_PLUG = AudioSystem.DEVICE_OUT_WIRED_HEADSET | AudioSystem.DEVICE_OUT_WIRED_HEADPHONE | AudioSystem.DEVICE_OUT_LINE | diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index d35f952a32ba..2f866327782d 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -15032,24 +15032,26 @@ public class PackageManagerService extends IPackageManager.Stub void tryProcessInstallRequest(InstallArgs args, int currentStatus) { mCurrentState.put(args, currentStatus); - boolean success = true; if (mCurrentState.size() != mChildParams.size()) { return; } + int completeStatus = PackageManager.INSTALL_SUCCEEDED; for (Integer status : mCurrentState.values()) { if (status == PackageManager.INSTALL_UNKNOWN) { return; } else if (status != PackageManager.INSTALL_SUCCEEDED) { - success = false; + completeStatus = status; break; } } final List<InstallRequest> installRequests = new ArrayList<>(mCurrentState.size()); for (Map.Entry<InstallArgs, Integer> entry : mCurrentState.entrySet()) { installRequests.add(new InstallRequest(entry.getKey(), - createPackageInstalledInfo(entry.getValue()))); + createPackageInstalledInfo(completeStatus))); } - processInstallRequestsAsync(success, installRequests); + processInstallRequestsAsync( + completeStatus == PackageManager.INSTALL_SUCCEEDED, + installRequests); } } diff --git a/services/core/java/com/android/server/power/AttentionDetector.java b/services/core/java/com/android/server/power/AttentionDetector.java index 14f1196ab3a2..ed11fd45ec39 100644 --- a/services/core/java/com/android/server/power/AttentionDetector.java +++ b/services/core/java/com/android/server/power/AttentionDetector.java @@ -19,6 +19,8 @@ package com.android.server.power; import static android.provider.Settings.System.ADAPTIVE_SLEEP; import android.Manifest; +import android.app.ActivityManager; +import android.app.SynchronousUserSwitchObserver; import android.attention.AttentionManagerInternal; import android.attention.AttentionManagerInternal.AttentionCallbackInternal; import android.content.ContentResolver; @@ -28,6 +30,7 @@ import android.database.ContentObserver; import android.os.Handler; import android.os.PowerManager; import android.os.PowerManagerInternal; +import android.os.RemoteException; import android.os.SystemClock; import android.os.UserHandle; import android.provider.Settings; @@ -54,6 +57,8 @@ public class AttentionDetector { private static final String TAG = "AttentionDetector"; private static final boolean DEBUG = false; + private Context mContext; + private boolean mIsSettingEnabled; /** @@ -132,6 +137,7 @@ public class AttentionDetector { } public void systemReady(Context context) { + mContext = context; updateEnabledFromSettings(context); mPackageManager = context.getPackageManager(); mContentResolver = context.getContentResolver(); @@ -141,6 +147,13 @@ public class AttentionDetector { mMaxAttentionApiTimeoutMillis = context.getResources().getInteger( com.android.internal.R.integer.config_attentionApiTimeout); + try { + final UserSwitchObserver observer = new UserSwitchObserver(); + ActivityManager.getService().registerUserSwitchObserver(observer, TAG); + } catch (RemoteException e) { + // Shouldn't happen since in-process. + } + context.getContentResolver().registerContentObserver(Settings.System.getUriFor( Settings.System.ADAPTIVE_SLEEP), false, new ContentObserver(new Handler()) { @@ -326,4 +339,11 @@ public class AttentionDetector { mRequested.set(false); } } + + private final class UserSwitchObserver extends SynchronousUserSwitchObserver { + @Override + public void onUserSwitching(int newUserId) throws RemoteException { + updateEnabledFromSettings(mContext); + } + } } diff --git a/services/net/java/android/net/NetworkStackClient.java b/services/net/java/android/net/NetworkStackClient.java index 6b5842ff9065..09c9b6d360a9 100644 --- a/services/net/java/android/net/NetworkStackClient.java +++ b/services/net/java/android/net/NetworkStackClient.java @@ -26,22 +26,27 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; +import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.net.dhcp.DhcpServingParamsParcel; import android.net.dhcp.IDhcpServerCallbacks; import android.net.ip.IIpClientCallbacks; import android.net.util.SharedLog; import android.os.Binder; -import android.os.Build; +import android.os.Environment; import android.os.IBinder; import android.os.Process; import android.os.RemoteException; import android.os.ServiceManager; +import android.os.SystemClock; import android.os.UserHandle; +import android.provider.DeviceConfig; +import android.util.ArraySet; import android.util.Slog; import com.android.internal.annotations.GuardedBy; +import java.io.File; import java.io.PrintWriter; import java.util.ArrayList; @@ -54,6 +59,15 @@ public class NetworkStackClient { private static final int NETWORKSTACK_TIMEOUT_MS = 10_000; private static final String IN_PROCESS_SUFFIX = ".InProcess"; + private static final String PREFS_FILE = "NetworkStackClientPrefs.xml"; + private static final String PREF_KEY_LAST_CRASH_UPTIME = "lastcrash"; + private static final String CONFIG_MIN_CRASH_INTERVAL_MS = "min_crash_interval"; + + // Even if the network stack is lost, do not crash the system more often than this. + // Connectivity would be broken, but if the user needs the device for something urgent + // (like calling emergency services) we should not bootloop the device. + // This is the default value: the actual value can be adjusted via device config. + private static final long DEFAULT_MIN_CRASH_INTERVAL_MS = 6 * 3_600_000L; private static NetworkStackClient sInstance; @@ -67,12 +81,34 @@ public class NetworkStackClient { @GuardedBy("mLog") private final SharedLog mLog = new SharedLog(TAG); - private volatile boolean mNetworkStackStartRequested = false; + private volatile boolean mWasSystemServerInitialized = false; + + /** + * If non-zero, indicates that the last framework start happened after a crash of the + * NetworkStack which was at the specified uptime. + */ + private volatile long mLastCrashUptime = 0L; + + @GuardedBy("mHealthListeners") + private final ArraySet<NetworkStackHealthListener> mHealthListeners = new ArraySet<>(); private interface NetworkStackCallback { void onNetworkStackConnected(INetworkStackConnector connector); } + /** + * Callback interface for severe failures of the NetworkStack. + * + * <p>Useful for health monitors such as PackageWatchdog. + */ + public interface NetworkStackHealthListener { + /** + * Called when there is a severe failure of the network stack. + * @param packageName Package name of the network stack. + */ + void onNetworkStackFailure(@NonNull String packageName); + } + private NetworkStackClient() { } /** @@ -86,6 +122,15 @@ public class NetworkStackClient { } /** + * Add a {@link NetworkStackHealthListener} to listen to network stack health events. + */ + public void registerHealthListener(@NonNull NetworkStackHealthListener listener) { + synchronized (mHealthListeners) { + mHealthListeners.add(listener); + } + } + + /** * Create a DHCP server according to the specified parameters. * * <p>The server will be returned asynchronously through the provided callbacks. @@ -147,6 +192,16 @@ public class NetworkStackClient { } private class NetworkStackConnection implements ServiceConnection { + @NonNull + private final Context mContext; + @NonNull + private final String mPackageName; + + private NetworkStackConnection(@NonNull Context context, @NonNull String packageName) { + mContext = context; + mPackageName = packageName; + } + @Override public void onServiceConnected(ComponentName name, IBinder service) { logi("Network stack service connected"); @@ -155,14 +210,14 @@ public class NetworkStackClient { @Override public void onServiceDisconnected(ComponentName name) { - // The system has lost its network stack (probably due to a crash in the - // network stack process): better crash rather than stay in a bad state where all - // networking is broken. // onServiceDisconnected is not being called on device shutdown, so this method being // called always indicates a bad state for the system server. - maybeCrashWithTerribleFailure("Lost network stack"); + // This code path is only run by the system server: only the system server binds + // to the NetworkStack as a service. Other processes get the NetworkStack from + // the ServiceManager. + maybeCrashWithTerribleFailure("Lost network stack", mContext, mPackageName); } - }; + } private void registerNetworkStackService(@NonNull IBinder service) { final INetworkStackConnector connector = INetworkStackConnector.Stub.asInterface(service); @@ -189,7 +244,7 @@ public class NetworkStackClient { */ public void init() { log("Network stack init"); - mNetworkStackStartRequested = true; + mWasSystemServerInitialized = true; } /** @@ -202,6 +257,13 @@ public class NetworkStackClient { */ public void start(Context context) { log("Starting network stack"); + + final SharedPreferences prefs = getSharedPreferences(context); + mLastCrashUptime = prefs.getLong(PREF_KEY_LAST_CRASH_UPTIME, 0L); + // Remove the preference after getting the last crash uptime, so mLastCrashUptime always + // indicates this is the first start since the last crash. + prefs.edit().remove(PREF_KEY_LAST_CRASH_UPTIME).commit(); + final PackageManager pm = context.getPackageManager(); // Try to bind in-process if the device was shipped with an in-process version @@ -216,16 +278,19 @@ public class NetworkStackClient { } if (intent == null) { - maybeCrashWithTerribleFailure("Could not resolve the network stack"); + maybeCrashWithTerribleFailure("Could not resolve the network stack", context, null); return; } + final String packageName = intent.getComponent().getPackageName(); + // Start the network stack. The service will be added to the service manager in // NetworkStackConnection.onServiceConnected(). - if (!context.bindServiceAsUser(intent, new NetworkStackConnection(), + if (!context.bindServiceAsUser(intent, new NetworkStackConnection(context, packageName), Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT, UserHandle.SYSTEM)) { maybeCrashWithTerribleFailure( - "Could not bind to network stack in-process, or in app with " + intent); + "Could not bind to network stack in-process, or in app with " + intent, + context, packageName); return; } @@ -274,11 +339,47 @@ public class NetworkStackClient { } } - private void maybeCrashWithTerribleFailure(@NonNull String message) { + private void maybeCrashWithTerribleFailure(@NonNull String message, + @NonNull Context context, @Nullable String packageName) { logWtf(message, null); - if (Build.IS_DEBUGGABLE) { + // uptime is monotonic even after a framework restart + final long uptime = SystemClock.elapsedRealtime(); + final long minCrashIntervalMs = DeviceConfig.getLong(DeviceConfig.NAMESPACE_CONNECTIVITY, + CONFIG_MIN_CRASH_INTERVAL_MS, DEFAULT_MIN_CRASH_INTERVAL_MS); + + // Either the framework was not restarted after a crash of the NetworkStack, or the min + // crash interval has passed since then. + if (mLastCrashUptime == 0L || uptime - mLastCrashUptime > minCrashIntervalMs) { + // The system is not bound to its network stack (for example due to a crash in the + // network stack process): better crash rather than stay in a bad state where all + // networking is broken. + // Using device-encrypted SharedPreferences as DeviceConfig does not have a synchronous + // API to persist settings before a crash. + final SharedPreferences prefs = getSharedPreferences(context); + if (!prefs.edit().putLong(PREF_KEY_LAST_CRASH_UPTIME, uptime).commit()) { + logWtf("Could not persist last crash uptime", null); + } throw new IllegalStateException(message); } + + // Here the system crashed recently already. Inform listeners that something is + // definitely wrong. + if (packageName != null) { + final ArraySet<NetworkStackHealthListener> listeners; + synchronized (mHealthListeners) { + listeners = new ArraySet<>(mHealthListeners); + } + for (NetworkStackHealthListener listener : listeners) { + listener.onNetworkStackFailure(packageName); + } + } + } + + private SharedPreferences getSharedPreferences(Context context) { + final File prefsFile = new File( + Environment.getDataSystemDeDirectory(UserHandle.USER_SYSTEM), PREFS_FILE); + return context.createDeviceProtectedStorageContext() + .getSharedPreferences(prefsFile, Context.MODE_PRIVATE); } /** @@ -350,7 +451,7 @@ public class NetworkStackClient { "Only the system server should try to bind to the network stack."); } - if (!mNetworkStackStartRequested) { + if (!mWasSystemServerInitialized) { // The network stack is not being started in this process, e.g. this process is not // the system server. Get a remote connector registered by the system server. final INetworkStackConnector connector = getRemoteConnector(); diff --git a/tests/PackageWatchdog/Android.bp b/tests/PackageWatchdog/Android.bp index b07996568839..88d92c4d94fe 100644 --- a/tests/PackageWatchdog/Android.bp +++ b/tests/PackageWatchdog/Android.bp @@ -18,11 +18,18 @@ android_test { srcs: ["src/**/*.java"], static_libs: [ "junit", + "mockito-target-extended-minus-junit4", "frameworks-base-testutils", "androidx.test.rules", "services.core", + "services.net", ], libs: ["android.test.runner"], + jni_libs: [ + // mockito-target-extended dependencies + "libdexmakerjvmtiagent", + "libstaticjvmtiagent", + ], platform_apis: true, test_suites: ["device-tests"], } diff --git a/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java b/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java index eb19361d86a3..97716377ea2e 100644 --- a/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java +++ b/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java @@ -27,6 +27,7 @@ import static org.junit.Assert.fail; import android.Manifest; import android.content.Context; import android.content.pm.VersionedPackage; +import android.net.NetworkStackClient; import android.os.Handler; import android.os.test.TestLooper; import android.provider.DeviceConfig; @@ -41,6 +42,8 @@ import com.android.server.PackageWatchdog.PackageHealthObserverImpact; import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; import java.io.File; import java.util.ArrayList; @@ -70,9 +73,12 @@ public class PackageWatchdogTest { private static final long SHORT_DURATION = TimeUnit.SECONDS.toMillis(1); private static final long LONG_DURATION = TimeUnit.SECONDS.toMillis(5); private TestLooper mTestLooper; + @Mock + private NetworkStackClient mNetworkStackClient; @Before public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); new File(InstrumentationRegistry.getContext().getFilesDir(), "package-watchdog.xml").delete(); adoptShellPermissions(Manifest.permission.READ_DEVICE_CONFIG); @@ -732,7 +738,8 @@ public class PackageWatchdogTest { new AtomicFile(new File(context.getFilesDir(), "package-watchdog.xml")); Handler handler = new Handler(mTestLooper.getLooper()); PackageWatchdog watchdog = - new PackageWatchdog(context, policyFile, handler, handler, controller); + new PackageWatchdog(context, policyFile, handler, handler, controller, + mNetworkStackClient); // Verify controller is not automatically started assertFalse(controller.mIsEnabled); if (withPackagesReady) { diff --git a/tests/RollbackTest/Android.bp b/tests/RollbackTest/Android.bp index 1cf3d364a8d5..aec40558ad51 100644 --- a/tests/RollbackTest/Android.bp +++ b/tests/RollbackTest/Android.bp @@ -95,7 +95,7 @@ android_test { ":RollbackTestAppASplitV2", ], test_config: "RollbackTest.xml", - sdk_version: "test_current", + // TODO: sdk_version: "test_current" when Intent#resolveSystemservice is TestApi } java_test_host { diff --git a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/RollbackTestUtils.java b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/RollbackTestUtils.java index 81629aaaec76..a9e20cdb191b 100644 --- a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/RollbackTestUtils.java +++ b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/RollbackTestUtils.java @@ -28,6 +28,7 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageInstaller; import android.content.pm.PackageManager; @@ -82,13 +83,31 @@ class RollbackTestUtils { * Returns -1 if the package is not currently installed. */ static long getInstalledVersion(String packageName) { + PackageInfo pi = getPackageInfo(packageName); + if (pi == null) { + return -1; + } else { + return pi.getLongVersionCode(); + } + } + + private static boolean isSystemAppWithoutUpdate(String packageName) { + PackageInfo pi = getPackageInfo(packageName); + if (pi == null) { + return false; + } else { + return ((pi.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) + && ((pi.applicationInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) == 0); + } + } + + private static PackageInfo getPackageInfo(String packageName) { Context context = InstrumentationRegistry.getContext(); PackageManager pm = context.getPackageManager(); try { - PackageInfo info = pm.getPackageInfo(packageName, PackageManager.MATCH_APEX); - return info.getLongVersionCode(); + return pm.getPackageInfo(packageName, PackageManager.MATCH_APEX); } catch (PackageManager.NameNotFoundException e) { - return -1; + return null; } } @@ -109,8 +128,8 @@ class RollbackTestUtils { * @throws AssertionError if package can't be uninstalled. */ static void uninstall(String packageName) throws InterruptedException, IOException { - // No need to uninstall if the package isn't installed. - if (getInstalledVersion(packageName) == -1) { + // No need to uninstall if the package isn't installed or is installed on /system. + if (getInstalledVersion(packageName) == -1 || isSystemAppWithoutUpdate(packageName)) { return; } diff --git a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/StagedRollbackTest.java b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/StagedRollbackTest.java index 1ddfa6ee54ce..1a29c4c11457 100644 --- a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/StagedRollbackTest.java +++ b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/StagedRollbackTest.java @@ -21,7 +21,9 @@ import static com.android.tests.rollback.RollbackTestUtils.getUniqueRollbackInfo import android.Manifest; import android.content.BroadcastReceiver; +import android.content.ComponentName; import android.content.Context; +import android.content.Intent; import android.content.pm.VersionedPackage; import android.content.rollback.RollbackInfo; import android.content.rollback.RollbackManager; @@ -30,6 +32,8 @@ import androidx.test.InstrumentationRegistry; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -54,6 +58,8 @@ public class StagedRollbackTest { private static final String TEST_APP_A = "com.android.tests.rollback.testapp.A"; private static final String TEST_APP_A_V1 = "RollbackTestAppAv1.apk"; private static final String TEST_APP_A_CRASHING_V2 = "RollbackTestAppACrashingV2.apk"; + private static final String NETWORK_STACK_CONNECTOR_CLASS = + "android.net.INetworkStackConnector"; /** * Adopts common shell permissions needed for rollback tests. @@ -157,4 +163,44 @@ public class StagedRollbackTest { assertTrue(rollback.isStaged()); assertNotEquals(-1, rollback.getCommittedSessionId()); } + + @Test + public void resetNetworkStack() throws Exception { + RollbackManager rm = RollbackTestUtils.getRollbackManager(); + String networkStack = getNetworkStackPackageName(); + + rm.expireRollbackForPackage(networkStack); + RollbackTestUtils.uninstall(networkStack); + + assertNull(getUniqueRollbackInfoForPackage(rm.getAvailableRollbacks(), + networkStack)); + } + + @Test + public void assertNetworkStackRollbackAvailable() throws Exception { + RollbackManager rm = RollbackTestUtils.getRollbackManager(); + assertNotNull(getUniqueRollbackInfoForPackage(rm.getAvailableRollbacks(), + getNetworkStackPackageName())); + } + + @Test + public void assertNetworkStackRollbackCommitted() throws Exception { + RollbackManager rm = RollbackTestUtils.getRollbackManager(); + assertNotNull(getUniqueRollbackInfoForPackage(rm.getRecentlyCommittedRollbacks(), + getNetworkStackPackageName())); + } + + @Test + public void assertNoNetworkStackRollbackCommitted() throws Exception { + RollbackManager rm = RollbackTestUtils.getRollbackManager(); + assertNull(getUniqueRollbackInfoForPackage(rm.getRecentlyCommittedRollbacks(), + getNetworkStackPackageName())); + } + + private String getNetworkStackPackageName() { + Intent intent = new Intent(NETWORK_STACK_CONNECTOR_CLASS); + ComponentName comp = intent.resolveSystemService( + InstrumentationRegistry.getContext().getPackageManager(), 0); + return comp.getPackageName(); + } } diff --git a/tests/RollbackTest/StagedRollbackTest/src/com/android/tests/rollback/host/StagedRollbackTest.java b/tests/RollbackTest/StagedRollbackTest/src/com/android/tests/rollback/host/StagedRollbackTest.java index 75a95adf460a..bad294794337 100644 --- a/tests/RollbackTest/StagedRollbackTest/src/com/android/tests/rollback/host/StagedRollbackTest.java +++ b/tests/RollbackTest/StagedRollbackTest/src/com/android/tests/rollback/host/StagedRollbackTest.java @@ -23,6 +23,8 @@ import com.android.tradefed.log.LogUtil.CLog; import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test; +import org.junit.After; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -43,6 +45,20 @@ public class StagedRollbackTest extends BaseHostJUnit4Test { phase)); } + @Before + public void setUp() throws Exception { + // Disconnect internet so we can test network health triggered rollbacks + getDevice().executeShellCommand("svc wifi disable"); + getDevice().executeShellCommand("svc data disable"); + } + + @After + public void tearDown() throws Exception { + // Reconnect internet after testing network health triggered rollbacks + getDevice().executeShellCommand("svc wifi enable"); + getDevice().executeShellCommand("svc data enable"); + } + /** * Tests watchdog triggered staged rollbacks involving only apks. */ @@ -63,6 +79,90 @@ public class StagedRollbackTest extends BaseHostJUnit4Test { } getDevice().waitForDeviceAvailable(); + runPhase("testBadApkOnlyConfirmRollback"); } + + /** + * Tests failed network health check triggers watchdog staged rollbacks. + */ + @Test + public void testNetworkFailedRollback() throws Exception { + // Remove available rollbacks and uninstall NetworkStack on /data/ + runPhase("resetNetworkStack"); + // Reduce health check deadline + getDevice().executeShellCommand("device_config put rollback " + + "watchdog_request_timeout_millis 300000"); + // Simulate re-installation of new NetworkStack with rollbacks enabled + getDevice().executeShellCommand("pm install -r --staged --enable-rollback " + + "/system/priv-app/NetworkStack/NetworkStack.apk"); + + // Sleep to allow writes to disk before reboot + Thread.sleep(5000); + // Reboot device to activate staged package + getDevice().reboot(); + getDevice().waitForDeviceAvailable(); + + // Verify rollback was enabled + runPhase("assertNetworkStackRollbackAvailable"); + + // Sleep for < health check deadline + Thread.sleep(5000); + // Verify rollback was not executed before health check deadline + runPhase("assertNoNetworkStackRollbackCommitted"); + try { + // This is expected to fail due to the device being rebooted out + // from underneath the test. If this fails for reasons other than + // the device reboot, those failures should result in failure of + // the assertNetworkStackExecutedRollback phase. + CLog.logAndDisplay(LogLevel.INFO, "Sleep and expect to fail while sleeping"); + // Sleep for > health check deadline + Thread.sleep(260000); + } catch (AssertionError e) { + // AssertionError is expected. + } + + getDevice().waitForDeviceAvailable(); + // Verify rollback was executed after health check deadline + runPhase("assertNetworkStackRollbackCommitted"); + } + + /** + * Tests passed network health check does not trigger watchdog staged rollbacks. + */ + @Test + public void testNetworkPassedDoesNotRollback() throws Exception { + // Remove available rollbacks and uninstall NetworkStack on /data/ + runPhase("resetNetworkStack"); + // Reduce health check deadline, here unlike the network failed case, we use + // a longer deadline because joining a network can take a much longer time for + // reasons external to the device than 'not joining' + getDevice().executeShellCommand("device_config put rollback " + + "watchdog_request_timeout_millis 300000"); + // Simulate re-installation of new NetworkStack with rollbacks enabled + getDevice().executeShellCommand("pm install -r --staged --enable-rollback " + + "/system/priv-app/NetworkStack/NetworkStack.apk"); + + // Sleep to allow writes to disk before reboot + Thread.sleep(5000); + // Reboot device to activate staged package + getDevice().reboot(); + getDevice().waitForDeviceAvailable(); + + // Verify rollback was enabled + runPhase("assertNetworkStackRollbackAvailable"); + + // Connect to internet so network health check passes + getDevice().executeShellCommand("svc wifi enable"); + getDevice().executeShellCommand("svc data enable"); + + // Wait for device available because emulator device may restart after turning + // on mobile data + getDevice().waitForDeviceAvailable(); + + // Sleep for > health check deadline + Thread.sleep(310000); + // Verify rollback was not executed after health check deadline + runPhase("assertNoNetworkStackRollbackCommitted"); + } } |