Merge "Fixes test that is currently failing" into qt-dev
diff --git a/cmds/bootanimation/BootAnimation.cpp b/cmds/bootanimation/BootAnimation.cpp
index 16af071..30f4ad4 100644
--- a/cmds/bootanimation/BootAnimation.cpp
+++ b/cmds/bootanimation/BootAnimation.cpp
@@ -69,6 +69,7 @@
static const char PRODUCT_BOOTANIMATION_DARK_FILE[] = "/product/media/bootanimation-dark.zip";
static const char PRODUCT_BOOTANIMATION_FILE[] = "/product/media/bootanimation.zip";
static const char SYSTEM_BOOTANIMATION_FILE[] = "/system/media/bootanimation.zip";
+static const char APEX_BOOTANIMATION_FILE[] = "/apex/com.android.bootanimation/etc/bootanimation.zip";
static const char PRODUCT_ENCRYPTED_BOOTANIMATION_FILE[] = "/product/media/bootanimation-encrypted.zip";
static const char SYSTEM_ENCRYPTED_BOOTANIMATION_FILE[] = "/system/media/bootanimation-encrypted.zip";
static const char OEM_SHUTDOWNANIMATION_FILE[] = "/oem/media/shutdownanimation.zip";
@@ -358,10 +359,10 @@
const bool playDarkAnim = android::base::GetIntProperty("ro.boot.theme", 0) == 1;
static const char* bootFiles[] =
- {playDarkAnim ? PRODUCT_BOOTANIMATION_DARK_FILE : PRODUCT_BOOTANIMATION_FILE,
+ {APEX_BOOTANIMATION_FILE, playDarkAnim ? PRODUCT_BOOTANIMATION_DARK_FILE : PRODUCT_BOOTANIMATION_FILE,
OEM_BOOTANIMATION_FILE, SYSTEM_BOOTANIMATION_FILE};
static const char* shutdownFiles[] =
- {PRODUCT_SHUTDOWNANIMATION_FILE, OEM_SHUTDOWNANIMATION_FILE, SYSTEM_SHUTDOWNANIMATION_FILE};
+ {PRODUCT_SHUTDOWNANIMATION_FILE, OEM_SHUTDOWNANIMATION_FILE, SYSTEM_SHUTDOWNANIMATION_FILE, ""};
for (const char* f : (!mShuttingDown ? bootFiles : shutdownFiles)) {
if (access(f, R_OK) == 0) {
diff --git a/cmds/statsd/src/atoms.proto b/cmds/statsd/src/atoms.proto
index 2899d49..495a09f 100644
--- a/cmds/statsd/src/atoms.proto
+++ b/cmds/statsd/src/atoms.proto
@@ -48,6 +48,7 @@
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 @@
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 @@
// 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 44ea218..95a7973 100644
--- a/core/java/android/app/admin/DevicePolicyEventLogger.java
+++ b/core/java/android/app/admin/DevicePolicyEventLogger.java
@@ -22,9 +22,10 @@
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 @@
* @see StatsLog
* @hide
*/
-public final class DevicePolicyEventLogger {
+public class DevicePolicyEventLogger {
private final int mEventId;
private int mIntValue;
private boolean mBooleanValue;
@@ -71,7 +72,6 @@
/**
* Returns the event id.
*/
- @VisibleForTesting
public int getEventId() {
return mEventId;
}
@@ -87,7 +87,6 @@
/**
* Returns the generic <code>int</code> value.
*/
- @VisibleForTesting
public int getInt() {
return mIntValue;
}
@@ -103,7 +102,6 @@
/**
* Returns the generic <code>boolean</code> value.
*/
- @VisibleForTesting
public boolean getBoolean() {
return mBooleanValue;
}
@@ -119,7 +117,6 @@
/**
* Returns the time period in milliseconds.
*/
- @VisibleForTesting
public long getTimePeriod() {
return mTimePeriodMs;
}
@@ -162,11 +159,13 @@
}
/**
- * 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 @@
/**
* 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 ab630fd..82d4d1d 100644
--- a/core/java/android/inputmethodservice/InputMethodService.java
+++ b/core/java/android/inputmethodservice/InputMethodService.java
@@ -472,8 +472,12 @@
*/
@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/com/android/internal/inputmethod/InputMethodPrivilegedOperationsRegistry.java b/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperationsRegistry.java
index 1436aed..049f952 100644
--- a/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperationsRegistry.java
+++ b/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperationsRegistry.java
@@ -132,4 +132,21 @@
}
}
}
+
+ /**
+ * 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/proto/android/stats/location/location_enums.proto b/core/proto/android/stats/location/location_enums.proto
new file mode 100644
index 0000000..553c01c
--- /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 fb58569..2beef42 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -1276,9 +1276,14 @@
meanings. -->
<integer name="config_defaultRingVibrationIntensity">2</integer>
+ <!-- Whether to use the strict phone number matcher by default. -->
<bool name="config_use_strict_phone_number_comparation">false</bool>
- <bool name="config_use_strict_phone_number_comparation_for_russian">true</bool>
+ <!-- Whether to use the strict phone number matcher in Russia. -->
+ <bool name="config_use_strict_phone_number_comparation_for_russia">true</bool>
+
+ <!-- Whether to use the strict phone number matcher in Kazakhstan. -->
+ <bool name="config_use_strict_phone_number_comparation_for_kazakhstan">true</bool>
<!-- Display low battery warning when battery level dips to this value.
Also, the battery stats are flushed to disk when we hit this level. -->
@@ -3306,6 +3311,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 3d2d969..4af05f6 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -313,7 +313,8 @@
<java-symbol type="bool" name="config_ui_enableFadingMarquee" />
<java-symbol type="bool" name="config_enableHapticTextHandle" />
<java-symbol type="bool" name="config_use_strict_phone_number_comparation" />
- <java-symbol type="bool" name="config_use_strict_phone_number_comparation_for_russian" />
+ <java-symbol type="bool" name="config_use_strict_phone_number_comparation_for_russia" />
+ <java-symbol type="bool" name="config_use_strict_phone_number_comparation_for_kazakhstan" />
<java-symbol type="bool" name="config_single_volume" />
<java-symbol type="bool" name="config_voice_capable" />
<java-symbol type="bool" name="config_requireCallCapableAccountForHandle" />
@@ -2880,6 +2881,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/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index b3856d5..a640122 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -196,6 +196,7 @@
<permission name="android.permission.USE_RESERVED_DISK"/>
<permission name="android.permission.WRITE_MEDIA_STORAGE"/>
<permission name="android.permission.WATCH_APPOPS"/>
+ <permission name="android.permission.UPDATE_DEVICE_STATS"/>
</privapp-permissions>
<privapp-permissions package="com.android.providers.telephony">
diff --git a/graphics/java/android/graphics/Outline.java b/graphics/java/android/graphics/Outline.java
index 98c990a..1fc056c 100644
--- a/graphics/java/android/graphics/Outline.java
+++ b/graphics/java/android/graphics/Outline.java
@@ -273,8 +273,12 @@
}
/**
- * 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 @@
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 2541982..2d6cd24 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -4274,38 +4274,6 @@
}
}
- /**
- * 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 a790441..71f52a1 100644
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -178,9 +178,6 @@
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/car_ongoing_privacy_chip.xml b/packages/CarSystemUI/res/layout/car_ongoing_privacy_chip.xml
deleted file mode 100644
index 918abd9..0000000
--- a/packages/CarSystemUI/res/layout/car_ongoing_privacy_chip.xml
+++ /dev/null
@@ -1,37 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- 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.
--->
-
-<com.android.systemui.statusbar.car.privacy.OngoingPrivacyChip
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/car_privacy_chip"
- android:layout_width="wrap_content"
- android:layout_height="match_parent"
- android:layout_margin="@dimen/ongoing_appops_chip_margin"
- android:layout_toStartOf="@+id/clock_container"
- android:focusable="true"
- android:gravity="center_vertical|end"
- android:orientation="horizontal"
- android:paddingEnd="@dimen/ongoing_appops_chip_side_padding"
- android:paddingStart="@dimen/ongoing_appops_chip_side_padding"
- android:visibility="visible">
-
- <LinearLayout
- android:id="@+id/icons_container"
- android:layout_width="wrap_content"
- android:layout_height="match_parent"
- android:gravity="center_vertical|start"/>
-</com.android.systemui.statusbar.car.privacy.OngoingPrivacyChip>
\ No newline at end of file
diff --git a/packages/CarSystemUI/res/layout/car_top_navigation_bar.xml b/packages/CarSystemUI/res/layout/car_top_navigation_bar.xml
index cae89c1..925ccb4 100644
--- a/packages/CarSystemUI/res/layout/car_top_navigation_bar.xml
+++ b/packages/CarSystemUI/res/layout/car_top_navigation_bar.xml
@@ -65,8 +65,6 @@
/>
</FrameLayout>
- <include layout="@layout/car_ongoing_privacy_chip"/>
-
<FrameLayout
android:id="@+id/clock_container"
android:layout_width="wrap_content"
diff --git a/packages/CarSystemUI/res/layout/notification_center_activity.xml b/packages/CarSystemUI/res/layout/notification_center_activity.xml
index 55b0d87..0af74c4 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/res/values/dimens.xml b/packages/CarSystemUI/res/values/dimens.xml
index 7027ce3..0358357b 100644
--- a/packages/CarSystemUI/res/values/dimens.xml
+++ b/packages/CarSystemUI/res/values/dimens.xml
@@ -59,16 +59,6 @@
<dimen name="car_keyline_1">24dp</dimen>
<dimen name="car_keyline_2">96dp</dimen>
<dimen name="car_keyline_3">128dp</dimen>
- <dimen name="privacy_chip_icon_height">36dp</dimen>
- <dimen name="privacy_chip_icon_padding_left">0dp</dimen>
- <dimen name="privacy_chip_icon_padding_right">0dp</dimen>
- <dimen name="privacy_chip_icon_padding_top">0dp</dimen>
- <dimen name="privacy_chip_icon_padding_bottom">0dp</dimen>
-
- <dimen name="privacy_chip_text_padding_left">0dp</dimen>
- <dimen name="privacy_chip_text_padding_right">0dp</dimen>
- <dimen name="privacy_chip_text_padding_top">0dp</dimen>
- <dimen name="privacy_chip_text_padding_bottom">0dp</dimen>
<dimen name="privacy_chip_icon_max_height">100dp</dimen>
@@ -86,16 +76,6 @@
<dimen name="ongoing_appops_chip_bg_padding">4dp</dimen>
<!-- Radius of Ongoing App Ops chip corners -->
<dimen name="ongoing_appops_chip_bg_corner_radius">12dp</dimen>
- <!-- Start padding for the app icon displayed in the dialog -->
- <dimen name="privacy_dialog_app_icon_padding_start">40dp</dimen>
- <!-- End padding for the app opps icon displayed in the dialog -->
- <dimen name="privacy_dialog_app_ops_icon_padding_end">40dp</dimen>
- <!-- Top padding for the list of application displayed in the dialog -->
- <dimen name="privacy_dialog_app_list_padding_top">20dp</dimen>
- <!-- Top padding for the dialog container-->
- <dimen name="privacy_dialog_container_padding_top">10dp</dimen>
- <!-- Top padding for the dialog title-->
- <dimen name="privacy_dialog_title_padding_start">10dp</dimen>
<!-- Car volume dimens. -->
<dimen name="car_volume_item_height">@*android:dimen/car_single_line_list_item_height</dimen>
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 b6b34c7..7fbdc2e 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 @@
}
}
});
+
+ // 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 @@
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 @@
}
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 @@
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 @@
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 @@
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 @@
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 @@
}
/**
- * 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/CarSystemUI/src/com/android/systemui/statusbar/car/privacy/OngoingPrivacyChip.java b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/privacy/OngoingPrivacyChip.java
deleted file mode 100644
index ead1de2..0000000
--- a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/privacy/OngoingPrivacyChip.java
+++ /dev/null
@@ -1,228 +0,0 @@
-/*
- * 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.systemui.statusbar.car.privacy;
-
-import android.app.ActivityManager;
-import android.app.AppOpsManager;
-import android.content.Context;
-import android.content.Intent;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.UserHandle;
-import android.os.UserManager;
-import android.util.AttributeSet;
-import android.view.View;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-
-import com.android.systemui.Dependency;
-import com.android.systemui.R;
-import com.android.systemui.appops.AppOpItem;
-import com.android.systemui.appops.AppOpsController;
-import com.android.systemui.plugins.ActivityStarter;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Objects;
-import java.util.stream.Collectors;
-
-/**
- * Layout defining the privacy chip that will be displayed in CarStatusRar with the information for
- * which applications are using AppOpps permission fpr camera, mic and location.
- */
-public class OngoingPrivacyChip extends LinearLayout implements View.OnClickListener {
-
- private Context mContext;
-
- private LinearLayout mIconsContainer;
- private List<PrivacyItem> mPrivacyItems;
- private static AppOpsController sAppOpsController;
- private UserManager mUserManager;
- private int mCurrentUser;
- private List<Integer> mCurrentUserIds;
- private boolean mListening = false;
- PrivacyDialogBuilder mPrivacyDialogBuilder;
- private LinearLayout mPrivacyChip;
- private ActivityStarter mActivityStarter;
-
- protected static final int[] OPS = new int[]{
- AppOpsManager.OP_CAMERA,
- AppOpsManager.OP_RECORD_AUDIO,
- AppOpsManager.OP_COARSE_LOCATION,
- AppOpsManager.OP_FINE_LOCATION
- };
-
- public OngoingPrivacyChip(Context context) {
- super(context, null);
- init(context);
- }
-
- public OngoingPrivacyChip(Context context, AttributeSet attr) {
- super(context, attr);
- init(context);
- }
-
- public OngoingPrivacyChip(Context context, AttributeSet attr, int defStyle) {
- super(context, attr, defStyle);
- init(context);
- }
-
- public OngoingPrivacyChip(Context context, AttributeSet attr, int defStyle, int a) {
- super(context, attr, defStyle, a);
- init(context);
- }
-
- private void init(Context context) {
- mContext = context;
- mPrivacyItems = new ArrayList<>();
- sAppOpsController = Dependency.get(AppOpsController.class);
- mUserManager = mContext.getSystemService(UserManager.class);
- mActivityStarter = Dependency.get(ActivityStarter.class);
- mCurrentUser = ActivityManager.getCurrentUser();
- mCurrentUserIds = mUserManager.getProfiles(mCurrentUser).stream().map(
- userInfo -> userInfo.id).collect(Collectors.toList());
-
- mPrivacyDialogBuilder = new PrivacyDialogBuilder(context, mPrivacyItems);
- }
-
- private AppOpsController.Callback mCallback = new AppOpsController.Callback() {
-
- @Override
- public void onActiveStateChanged(int code, int uid, String packageName, boolean active) {
- int userId = UserHandle.getUserId(uid);
- if (mCurrentUserIds.contains(userId)) {
- updatePrivacyList();
- }
- }
- };
-
- @Override
- public void onFinishInflate() {
- mIconsContainer = findViewById(R.id.icons_container);
- mPrivacyChip = (LinearLayout) findViewById(R.id.car_privacy_chip);
- if (mPrivacyChip != null) {
- mPrivacyChip.setOnClickListener(this);
- setListening(true);
- }
- }
-
- @Override
- public void onDetachedFromWindow() {
- if (mPrivacyChip != null) {
- setListening(false);
- }
- super.onDetachedFromWindow();
- }
-
- @Override
- public void onClick(View v) {
- updatePrivacyList();
- Handler mUiHandler = new Handler(Looper.getMainLooper());
- mUiHandler.post(() -> {
- mActivityStarter.postStartActivityDismissingKeyguard(
- new Intent(Intent.ACTION_REVIEW_ONGOING_PERMISSION_USAGE), 0);
- });
- }
-
- private void setListening(boolean listen) {
- if (mListening == listen) {
- return;
- }
- mListening = listen;
- if (mListening) {
- sAppOpsController.addCallback(OPS, mCallback);
- updatePrivacyList();
- } else {
- sAppOpsController.removeCallback(OPS, mCallback);
- }
- }
-
- private void updatePrivacyList() {
- mPrivacyItems = mCurrentUserIds.stream()
- .flatMap(item -> sAppOpsController.getActiveAppOpsForUser(item).stream())
- .filter(Objects::nonNull)
- .map(item -> toPrivacyItem(item))
- .filter(Objects::nonNull)
- .collect(Collectors.toList());
- mPrivacyDialogBuilder = new PrivacyDialogBuilder(mContext, mPrivacyItems);
-
- Handler refresh = new Handler(Looper.getMainLooper());
- refresh.post(new Runnable() {
- @Override
- public void run() {
- updateView();
- }
- });
- }
-
- private PrivacyItem toPrivacyItem(AppOpItem appOpItem) {
- PrivacyType type;
- switch (appOpItem.getCode()) {
- case AppOpsManager.OP_CAMERA:
- type = PrivacyType.TYPE_CAMERA;
- break;
- case AppOpsManager.OP_COARSE_LOCATION:
- type = PrivacyType.TYPE_LOCATION;
- break;
- case AppOpsManager.OP_FINE_LOCATION:
- type = PrivacyType.TYPE_LOCATION;
- break;
- case AppOpsManager.OP_RECORD_AUDIO:
- type = PrivacyType.TYPE_MICROPHONE;
- break;
- default:
- return null;
- }
- PrivacyApplication app = new PrivacyApplication(appOpItem.getPackageName(), mContext);
- return new PrivacyItem(type, app, appOpItem.getTimeStarted());
- }
-
- // Should only be called if the mPrivacyDialogBuilder icons or app changed
- private void updateView() {
- if (mPrivacyItems.isEmpty()) {
- mPrivacyChip.setVisibility(GONE);
- return;
- }
- mPrivacyChip.setVisibility(VISIBLE);
- setIcons(mPrivacyDialogBuilder);
-
- requestLayout();
- }
-
- private void setIcons(PrivacyDialogBuilder dialogBuilder) {
- mIconsContainer.removeAllViews();
- dialogBuilder.generateIcons().forEach(item -> {
- int size = mContext.getResources().getDimensionPixelSize(
- R.dimen.privacy_chip_icon_height);
- ImageView image = new ImageView(mContext);
- image.setImageDrawable(item);
- LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(size, size);
-
- int leftPadding = mContext.getResources().getDimensionPixelSize(
- R.dimen.privacy_chip_icon_padding_left);
- int topPadding = mContext.getResources().getDimensionPixelSize(
- R.dimen.privacy_chip_icon_padding_top);
- int rightPadding = mContext.getResources().getDimensionPixelSize(
- R.dimen.privacy_chip_icon_padding_right);
- int bottomPadding = mContext.getResources().getDimensionPixelSize(
- R.dimen.privacy_chip_icon_padding_bottom);
- image.setLayoutParams(layoutParams);
- image.setPadding(leftPadding, topPadding, rightPadding, bottomPadding);
- mIconsContainer.addView(image);
- });
- }
-}
diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/privacy/PrivacyApplication.java b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/privacy/PrivacyApplication.java
deleted file mode 100644
index 5ec7a77..0000000
--- a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/privacy/PrivacyApplication.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * 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.systemui.statusbar.car.privacy;
-
-import android.car.userlib.CarUserManagerHelper;
-import android.content.Context;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager;
-import android.graphics.drawable.Drawable;
-import android.util.Log;
-
-/**
- * Class to hold the data for the applications that are using the AppOps permissions.
- */
-public class PrivacyApplication {
- private static final String TAG = "PrivacyApplication";
-
- private Drawable mIcon;
- private String mApplicationName;
-
- public PrivacyApplication(String packageName, Context context) {
- try {
- CarUserManagerHelper carUserManagerHelper = new CarUserManagerHelper(context);
- ApplicationInfo app = context.getPackageManager()
- .getApplicationInfoAsUser(packageName, 0,
- carUserManagerHelper.getCurrentForegroundUserId());
- mIcon = context.getPackageManager().getApplicationIcon(app);
- mApplicationName = context.getPackageManager().getApplicationLabel(app).toString();
- } catch (PackageManager.NameNotFoundException e) {
- mApplicationName = packageName;
- Log.e(TAG, "Failed to to find package name", e);
- }
- }
-
- /**
- * Gets the application name.
- */
- public Drawable getIcon() {
- return mIcon;
- }
-
- /**
- * Gets the application name.
- */
- public String getApplicationName() {
- return mApplicationName;
- }
-}
diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/privacy/PrivacyDialogBuilder.java b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/privacy/PrivacyDialogBuilder.java
deleted file mode 100644
index 3b83e7c..0000000
--- a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/privacy/PrivacyDialogBuilder.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * 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.systemui.statusbar.car.privacy;
-
-import android.content.Context;
-import android.graphics.drawable.Drawable;
-
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.stream.Collectors;
-
-/**
- * Helper class to build the {@link OngoingPrivacyDialog}
- */
-public class PrivacyDialogBuilder {
-
- private Map<PrivacyType, List<PrivacyItem>> mItemsByType;
- private PrivacyApplication mApplication;
- private Context mContext;
-
- public PrivacyDialogBuilder(Context context, List<PrivacyItem> itemsList) {
- mContext = context;
- mItemsByType = itemsList.stream().filter(Objects::nonNull).collect(
- Collectors.groupingBy(PrivacyItem::getPrivacyType));
- List<PrivacyApplication> apps = itemsList.stream().filter(Objects::nonNull).map(
- PrivacyItem::getPrivacyApplication).distinct().collect(Collectors.toList());
- mApplication = apps.size() == 1 ? apps.get(0) : null;
- }
-
- /**
- * Gets the icon id for all the {@link PrivacyItem} in the same order as of itemList.
- */
- public List<Drawable> generateIcons() {
- return mItemsByType.keySet().stream().map(item -> item.getIconId(mContext)).collect(
- Collectors.toList());
- }
-
- /**
- * Gets the application object.
- */
- public PrivacyApplication getApplication() {
- return mApplication;
- }
-}
diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/privacy/PrivacyItem.java b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/privacy/PrivacyItem.java
deleted file mode 100644
index fca1373..0000000
--- a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/privacy/PrivacyItem.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * 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.systemui.statusbar.car.privacy;
-
-/**
- * Class for holding the data of each privacy item displayed in {@link OngoingPrivacyDialog}
- */
-public class PrivacyItem {
-
- private PrivacyType mPrivacyType;
- private PrivacyApplication mPrivacyApplication;
-
- public PrivacyItem(PrivacyType privacyType, PrivacyApplication privacyApplication,
- long timeStarted) {
- this.mPrivacyType = privacyType;
- this.mPrivacyApplication = privacyApplication;
- }
-
- /**
- * Gets the application object.
- */
- public PrivacyApplication getPrivacyApplication() {
- return mPrivacyApplication;
- }
-
- /**
- * Gets the privacy type for the application.
- */
- public PrivacyType getPrivacyType() {
- return mPrivacyType;
- }
-}
diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/privacy/PrivacyType.java b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/privacy/PrivacyType.java
deleted file mode 100644
index 8955c87..0000000
--- a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/privacy/PrivacyType.java
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * 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.systemui.statusbar.car.privacy;
-
-import android.content.Context;
-import android.graphics.drawable.Drawable;
-
-import com.android.systemui.R;
-
-/**
- * Enum for storing data for camera, mic and location.
- */
-public enum PrivacyType {
- TYPE_CAMERA(R.string.privacy_type_camera, com.android.internal.R.drawable.ic_camera),
- TYPE_LOCATION(R.string.privacy_type_location, R.drawable.stat_sys_location),
- TYPE_MICROPHONE(R.string.privacy_type_microphone, R.drawable.ic_mic_white);
-
- private int mNameId;
- private int mIconId;
-
- PrivacyType(int nameId, int iconId) {
- mNameId = nameId;
- mIconId = iconId;
- }
-
- /**
- * Get the icon Id.
- */
- public Drawable getIconId(Context context) {
- return context.getResources().getDrawable(mIconId, null);
- }
-
- /**
- * Get the name Id.
- */
- public String getNameId(Context context) {
- return context.getResources().getString(mNameId);
- }
-}
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index 9425941..91a8ab5 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -58,6 +58,7 @@
"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 @@
"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 3928062..456553d 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="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: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>
+ 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/res/layout/notification_info.xml b/packages/SystemUI/res/layout/notification_info.xml
index 8ffa2d8..87de9d4 100644
--- a/packages/SystemUI/res/layout/notification_info.xml
+++ b/packages/SystemUI/res/layout/notification_info.xml
@@ -219,7 +219,7 @@
android:gravity="center"
android:orientation="vertical">
- <LinearLayout
+ <com.android.systemui.statusbar.notification.row.ButtonLinearLayout
android:id="@+id/alert"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -268,9 +268,9 @@
android:ellipsize="end"
android:maxLines="2"
android:textAppearance="@style/TextAppearance.NotificationImportanceDetail"/>
- </LinearLayout>
+ </com.android.systemui.statusbar.notification.row.ButtonLinearLayout>
- <LinearLayout
+ <com.android.systemui.statusbar.notification.row.ButtonLinearLayout
android:id="@+id/silence"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -321,7 +321,7 @@
android:ellipsize="end"
android:maxLines="2"
android:textAppearance="@style/TextAppearance.NotificationImportanceDetail"/>
- </LinearLayout>
+ </com.android.systemui.statusbar.notification.row.ButtonLinearLayout>
</LinearLayout>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 8bc84c6..8174434 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2459,4 +2459,11 @@
<string name="bubble_accessibility_action_move_bottom_right">Move bottom right</string>
<!-- Text used for the bubble dismiss area. Bubbles dragged to, or flung towards, this area will go away. [CHAR LIMIT=20] -->
<string name="bubble_dismiss_text">Dismiss</string>
+
+ <!-- Notification content text when the system navigation mode changes as a result of changing the default launcher [CHAR LIMIT=NONE] -->
+ <string name="notification_content_system_nav_changed">System navigation updated. To make changes, go to Settings.</string>
+
+ <!-- Notification content text when switching to a default launcher that supports gesture navigation [CHAR LIMIT=NONE] -->
+ <string name="notification_content_gesture_nav_available">Go to Settings to update system navigation</string>
+
</resources>
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/InputChannelCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/InputChannelCompat.java
index 6567b6a..2b1fce8 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/InputChannelCompat.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/InputChannelCompat.java
@@ -18,7 +18,6 @@
import android.os.Bundle;
import android.os.Looper;
-import android.util.Pair;
import android.view.BatchedInputEventReceiver;
import android.view.Choreographer;
import android.view.InputChannel;
@@ -42,19 +41,6 @@
}
/**
- * Creates a dispatcher and receiver pair to better handle events across threads.
- */
- public static Pair<InputEventDispatcher, InputEventReceiver> createPair(String name,
- Looper looper, Choreographer choreographer, InputEventListener listener) {
- InputChannel[] channels = InputChannel.openInputChannelPair(name);
-
- InputEventDispatcher dispatcher = new InputEventDispatcher(channels[0], looper);
- InputEventReceiver receiver = new InputEventReceiver(channels[1], looper, choreographer,
- listener);
- return Pair.create(dispatcher, receiver);
- }
-
- /**
* Creates a dispatcher from the extras received as part on onInitialize
*/
public static InputEventReceiver fromBundle(Bundle params, String key,
diff --git a/packages/SystemUI/src/com/android/systemui/assist/ui/DefaultUiController.java b/packages/SystemUI/src/com/android/systemui/assist/ui/DefaultUiController.java
index 7ad6dfd..b1be811 100644
--- a/packages/SystemUI/src/com/android/systemui/assist/ui/DefaultUiController.java
+++ b/packages/SystemUI/src/com/android/systemui/assist/ui/DefaultUiController.java
@@ -35,6 +35,8 @@
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.systemui.Dependency;
import com.android.systemui.R;
+import com.android.systemui.ScreenDecorations;
+import com.android.systemui.SysUiServiceProvider;
import com.android.systemui.assist.AssistManager;
/**
@@ -92,12 +94,12 @@
if (progress == 1) {
animateInvocationCompletion(type, 0);
} else if (progress == 0) {
- mInvocationInProgress = false;
hide();
} else {
if (!mInvocationInProgress) {
attach();
mInvocationInProgress = true;
+ updateAssistHandleVisibility();
}
setProgressInternal(type, progress);
}
@@ -129,6 +131,7 @@
}
mInvocationLightsView.hide();
mInvocationInProgress = false;
+ updateAssistHandleVisibility();
}
/**
@@ -139,6 +142,12 @@
mInvocationLightsView.setColors(color1, color2, color3, color4);
}
+ private void updateAssistHandleVisibility() {
+ ScreenDecorations decorations = SysUiServiceProvider.getComponent(mRoot.getContext(),
+ ScreenDecorations.class);
+ decorations.setAssistHintBlocked(mInvocationInProgress);
+ }
+
private void attach() {
if (!mAttached) {
mWindowManager.addView(mRoot, mLayoutParams);
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
index c63389a..771df2d 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 @@
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 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 @@
};
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 @@
}
mStackAnimationController.cancelStackPositionAnimations();
- mBubbleContainer.setController(mStackAnimationController);
+ mBubbleContainer.setActiveController(mStackAnimationController);
hideFlyoutImmediate();
mDraggingInDismissTarget = false;
@@ -1111,6 +1107,10 @@
/** 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 @@
*/
void magnetToStackIfNeededThenAnimateDismissal(
View touchedView, float velX, float velY, Runnable after) {
+ final View draggedOutBubble = mExpandedAnimationController.getDraggedOutBubble();
final Runnable animateDismissal = () -> {
mAfterMagnet = null;
@@ -1218,7 +1219,7 @@
resetDesaturationAndDarken();
});
} else {
- mExpandedAnimationController.dismissDraggedOutBubble(() -> {
+ mExpandedAnimationController.dismissDraggedOutBubble(draggedOutBubble, () -> {
mAnimatingMagnet = false;
mShowingDismiss = false;
mDraggingInDismissTarget = false;
@@ -1385,10 +1386,18 @@
};
// 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 24337a3..1fa0e12 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.view.View;
import android.view.WindowInsets;
+import androidx.annotation.Nullable;
import androidx.dynamicanimation.animation.DynamicAnimation;
import androidx.dynamicanimation.animation.SpringForce;
@@ -63,12 +64,16 @@
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 @@
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.
- */
- private float mBubbleDraggingOutVelX;
- private float mBubbleDraggingOutVelY;
-
- @Override
- protected void setLayout(PhysicsAnimationLayout layout) {
- super.setLayout(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);
- mBubblesMaxRendered = res.getInteger(R.integer.bubbles_max_rendered);
- }
-
- /**
* Animates expanding the bubbles into a row along the top of the screen.
*/
- public void expandFromStack(PointF collapseTo, Runnable after) {
- animationsForChildrenFromIndex(
- 0, /* startIndex */
- new ChildAnimationConfigurator() {
- @Override
- public void configureAnimationForChildAtIndex(
- int index, PhysicsAnimationLayout.PhysicsPropertyAnimator animation) {
- animation.position(getBubbleLeft(index), getExpandedY());
- }
- })
- .startAll(after);
+ public void expandFromStack(Runnable after) {
+ mAnimatingCollapse = false;
+ mAnimatingExpand = true;
+ mAfterExpand = after;
- mCollapseToPoint = collapseTo;
+ startOrUpdateExpandAnimation();
}
/** Animate collapsing the bubbles back to their stacked position. */
- public void collapseBackToStack(Runnable after) {
- // Stack to the left if we're going to the left, or right if not.
- final float sideMultiplier = mLayout.isFirstChildXLeftOfCenter(mCollapseToPoint.x) ? -1 : 1;
+ public void collapseBackToStack(PointF collapsePoint, Runnable after) {
+ mAnimatingExpand = false;
+ mAnimatingCollapse = true;
+ mAfterCollapse = after;
+ mCollapsePoint = collapsePoint;
+ startOrUpdateCollapseAnimation();
+ }
+
+ private void startOrUpdateExpandAnimation() {
animationsForChildrenFromIndex(
0, /* startIndex */
- (index, animation) ->
+ (index, animation) -> animation.position(getBubbleLeft(index), getExpandedY()))
+ .startAll(() -> {
+ mAnimatingExpand = false;
+
+ if (mAfterExpand != null) {
+ mAfterExpand.run();
+ }
+
+ mAfterExpand = null;
+ });
+ }
+
+ private void startOrUpdateCollapseAnimation() {
+ // Stack to the left if we're going to the left, or right if not.
+ final float sideMultiplier = mLayout.isFirstChildXLeftOfCenter(mCollapsePoint.x) ? -1 : 1;
+ animationsForChildrenFromIndex(
+ 0, /* startIndex */
+ (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 @@
}
/** 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 @@
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 @@
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 @@
}
@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 @@
@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 @@
}
@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 @@
if (bubble.equals(mBubbleDraggingOut)) {
return;
}
+
animationForChild(bubble)
.translationX(getBubbleLeft(i))
.start();
@@ -403,10 +430,7 @@
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 997d2c4..3a33392 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 @@
*/
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 @@
}
/**
- * 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 @@
child.setTag(R.id.physics_animator_tag, animator);
}
+ animator.clearAnimator();
+ animator.setAssociatedController(this);
+
return animator;
}
@@ -235,32 +252,17 @@
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 @@
@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 @@
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 @@
// 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 @@
}
}
+ 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 @@
}
/**
+ * 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 @@
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 @@
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 @@
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 @@
*/
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 @@
/** 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 @@
/** 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 @@
/** 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 @@
/** 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 @@
/** 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 @@
* 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 @@
// 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 @@
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 @@
});
}
- 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 b9cdc844..ab8752e4 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 @@
/** 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 @@
},
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 @@
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 @@
Log.d(TAG, String.format("Setting position to (%f, %f).", pos.x, pos.y));
mStackPosition.set(pos.x, pos.y);
- mLayout.cancelAllAnimations();
- cancelStackPositionAnimations();
+ // 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));
+ // 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 @@
/** 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 ff16ed0..8647200 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 @@
// 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 @@
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 @@
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 03165f4..e1462d1 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 @@
listView.setReverseSublists(shouldReverseSublists());
listView.setReverseItems(shouldReverseListItems());
listView.setSwapRowsAndColumns(shouldSwapRowsAndColumns());
-
- fixNavBarClipping();
}
@Override
@@ -75,19 +73,6 @@
}
}
- /**
- * 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/notification/row/ButtonLinearLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ButtonLinearLayout.java
new file mode 100644
index 0000000..94bdd81
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ButtonLinearLayout.java
@@ -0,0 +1,32 @@
+/*
+ * 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.systemui.statusbar.notification.row;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.Button;
+import android.widget.LinearLayout;
+
+public class ButtonLinearLayout extends LinearLayout {
+
+ public ButtonLinearLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return Button.class.getName();
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/EdgeBackGestureHandler.java
index f221865..05a86fa 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/EdgeBackGestureHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/EdgeBackGestureHandler.java
@@ -33,13 +33,14 @@
import android.os.SystemClock;
import android.util.Log;
import android.util.MathUtils;
-import android.view.Choreographer;
import android.view.Gravity;
import android.view.IPinnedStackController;
import android.view.IPinnedStackListener;
import android.view.ISystemGestureExclusionListener;
+import android.view.InputChannel;
import android.view.InputDevice;
import android.view.InputEvent;
+import android.view.InputEventReceiver;
import android.view.InputMonitor;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
@@ -53,7 +54,6 @@
import com.android.systemui.R;
import com.android.systemui.bubbles.BubbleController;
import com.android.systemui.recents.OverviewProxyService;
-import com.android.systemui.shared.system.InputChannelCompat.InputEventReceiver;
import com.android.systemui.shared.system.QuickStepContract;
import com.android.systemui.shared.system.WindowManagerWrapper;
@@ -165,12 +165,14 @@
mEdgeWidth = res.getDimensionPixelSize(
com.android.internal.R.dimen.config_backGestureInset);
- mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
+ // Reduce the default touch slop to ensure that we can intercept the gesture
+ // before the app starts to react to it.
+ // TODO(b/130352502) Tune this value and extract into a constant
+ mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop() * 0.75f;
mLongPressTimeout = ViewConfiguration.getLongPressTimeout();
mNavBarHeight = res.getDimensionPixelSize(R.dimen.navigation_bar_frame_height);
- mMinArrowPosition = res.getDimensionPixelSize(
- R.dimen.navigation_edge_arrow_min_y);
+ mMinArrowPosition = res.getDimensionPixelSize(R.dimen.navigation_edge_arrow_min_y);
mFingerOffset = res.getDimensionPixelSize(R.dimen.navigation_edge_finger_offset);
}
@@ -250,9 +252,8 @@
// Register input event receiver
mInputMonitor = InputManager.getInstance().monitorGestureInput(
"edge-swipe", mDisplayId);
- mInputEventReceiver = new InputEventReceiver(mInputMonitor.getInputChannel(),
- Looper.getMainLooper(), Choreographer.getMainThreadInstance(),
- this::onInputEvent);
+ mInputEventReceiver = new SysUiInputEventReceiver(
+ mInputMonitor.getInputChannel(), Looper.getMainLooper());
// Add a nav bar panel window
mEdgePanel = new NavigationBarEdgePanel(mContext);
@@ -440,4 +441,15 @@
}
InputManager.getInstance().injectInputEvent(ev, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
}
+
+ class SysUiInputEventReceiver extends InputEventReceiver {
+ SysUiInputEventReceiver(InputChannel channel, Looper looper) {
+ super(channel, looper);
+ }
+
+ public void onInputEvent(InputEvent event) {
+ EdgeBackGestureHandler.this.onInputEvent(event);
+ finishInputEvent(event, true);
+ }
+ }
}
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 b7a154d..a0cda69 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.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 @@
private int[] mTmpTwoArray = new int[2];
private boolean mHeadsUpGoingAway;
private int mStatusBarState;
+ private Rect mTouchableRegion = new Rect();
private AnimationStateHandler mAnimationStateHandler;
@@ -297,10 +296,13 @@
@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 @@
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 @@
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 7623dee..92aa884 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 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 @@
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 @@
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 @@
updateHeader();
updateNotificationTranslucency();
updatePanelExpanded();
+ updateGestureExclusionRect();
if (DEBUG) {
invalidate();
}
@@ -2568,6 +2590,7 @@
mNotificationStackScroller.runAfterAnimationFinished(
mHeadsUpExistenceChangedRunnable);
}
+ updateGestureExclusionRect();
}
public void setHeadsUpAnimatingAway(boolean headsUpAnimatingAway) {
@@ -2992,6 +3015,7 @@
@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 756cf3e..b324235 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 @@
@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 @@
@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 @@
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 @@
// 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 @@
/** 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 @@
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 @@
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 eef6ddc..f8b32c2 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.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 @@
}
@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 @@
@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 @@
@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 @@
@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 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 @@
DynamicAnimation.SCALE_X, 10f);
secondController.setRemoveImmediately(true);
- mLayout.setController(secondController);
+ mLayout.setActiveController(secondController);
mTestableController.animationForChildAtIndex(0)
.scaleX(1.5f)
.start();
@@ -266,7 +253,7 @@
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 @@
@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 @@
@Test
public void testCancelAllAnimations() throws InterruptedException {
- mLayout.setController(mTestableController);
- addOneMoreThanRenderLimitBubbles();
+ mLayout.setActiveController(mTestableController);
+ addOneMoreThanBubbleLimitBubbles();
mTestableController.animationForChildAtIndex(0)
.position(1000, 1000)
@@ -321,29 +308,10 @@
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 @@
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 @@
@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 @@
// 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 @@
@Test
public void testAnimationsForChildrenFromIndex_noChildren() {
- mLayout.setController(mTestableController);
+ mLayout.setActiveController(mTestableController);
final Runnable after = Mockito.mock(Runnable.class);
mTestableController
@@ -523,8 +487,9 @@
}
@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 c6acef5d..f633f39 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 @@
Handler mMainThreadHandler;
- int mMaxRenderedBubbles;
int mSystemWindowInsetSize = 50;
int mCutoutInsetSize = 100;
@@ -69,6 +68,8 @@
@Mock
private DisplayCutout mCutout;
+ private int mMaxBubbles;
+
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
@@ -79,7 +80,7 @@
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 @@
}
/** 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 @@
}
@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 @@
}
@Override
- public void setController(PhysicsAnimationController controller) {
+ public void setActiveController(PhysicsAnimationController controller) {
runOnMainThreadAndBlock(
- () -> super.setController(
+ () -> super.setActiveController(
new MainThreadAnimationControllerWrapper(controller)));
}
@@ -267,8 +275,15 @@
}
@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 9218a8b..31a7d5a 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 @@
@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 a396f3e..ea47859 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 @@
@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 746140f..74e8cc2 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 @@
@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 d52fe81..ae04f76 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.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 @@
@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 @@
* 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 @@
"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 @@
}
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 @@
// 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 @@
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 @@
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 @@
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 @@
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 0000000..c503035
--- /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 3267114..dee89e5 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.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 @@
// 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 @@
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 @@
*/
@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 @@
() -> syncRequestsAsync());
setPropertyChangedListenerLocked();
updateConfigs();
+ registerNetworkStackHealthListener();
}
}
@@ -630,29 +636,40 @@
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 @@
}
}
+ 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 a0522e3..fcd6a0a 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 @@
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 @@
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 @@
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 @@
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 887c908..99b97cb 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 @@
}
}
- /*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 15e8851..30035c8 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 @@
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 d35f952..2f86632 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 @@
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 14f1196..ed11fd4 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 @@
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.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 @@
private static final String TAG = "AttentionDetector";
private static final boolean DEBUG = false;
+ private Context mContext;
+
private boolean mIsSettingEnabled;
/**
@@ -132,6 +137,7 @@
}
public void systemReady(Context context) {
+ mContext = context;
updateEnabledFromSettings(context);
mPackageManager = context.getPackageManager();
mContentResolver = context.getContentResolver();
@@ -141,6 +147,13 @@
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 @@
mRequested.set(false);
}
}
+
+ private final class UserSwitchObserver extends SynchronousUserSwitchObserver {
+ @Override
+ public void onUserSwitching(int newUserId) throws RemoteException {
+ updateEnabledFromSettings(mContext);
+ }
+ }
}
diff --git a/services/core/java/com/android/server/wm/AppWindowToken.java b/services/core/java/com/android/server/wm/AppWindowToken.java
index 3d7e50d..cae7612 100644
--- a/services/core/java/com/android/server/wm/AppWindowToken.java
+++ b/services/core/java/com/android/server/wm/AppWindowToken.java
@@ -1733,17 +1733,13 @@
return;
}
- if (mThumbnail == null && getTask() != null) {
- final TaskSnapshotController snapshotCtrl = mWmService.mTaskSnapshotController;
- final ArraySet<Task> tasks = new ArraySet<>();
- tasks.add(getTask());
- snapshotCtrl.snapshotTasks(tasks);
- snapshotCtrl.addSkipClosingAppSnapshotTasks(tasks);
- final ActivityManager.TaskSnapshot snapshot = snapshotCtrl.getSnapshot(
- getTask().mTaskId, getTask().mUserId, false /* restoreFromDisk */,
- false /* reducedResolution */);
+ Task task = getTask();
+ if (mThumbnail == null && task != null && !hasCommittedReparentToAnimationLeash()) {
+ SurfaceControl.ScreenshotGraphicBuffer snapshot =
+ mWmService.mTaskSnapshotController.createTaskSnapshot(
+ task, 1 /* scaleFraction */);
if (snapshot != null) {
- mThumbnail = new AppWindowThumbnail(t, this, snapshot.getSnapshot(),
+ mThumbnail = new AppWindowThumbnail(t, this, snapshot.getGraphicBuffer(),
true /* relative */);
}
}
@@ -2858,7 +2854,7 @@
}
}
t.hide(mTransitChangeLeash);
- t.reparent(mTransitChangeLeash, null);
+ t.remove(mTransitChangeLeash);
mTransitChangeLeash = null;
if (cancel) {
onAnimationLeashLost(t);
diff --git a/services/core/java/com/android/server/wm/TaskSnapshotController.java b/services/core/java/com/android/server/wm/TaskSnapshotController.java
index 432ca33..1815218 100644
--- a/services/core/java/com/android/server/wm/TaskSnapshotController.java
+++ b/services/core/java/com/android/server/wm/TaskSnapshotController.java
@@ -21,6 +21,7 @@
import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.ActivityManager.TaskSnapshot;
@@ -241,6 +242,28 @@
return null;
}
+ @Nullable
+ SurfaceControl.ScreenshotGraphicBuffer createTaskSnapshot(@NonNull Task task,
+ float scaleFraction) {
+ if (task.getSurfaceControl() == null) {
+ if (DEBUG_SCREENSHOT) {
+ Slog.w(TAG_WM, "Failed to take screenshot. No surface control for " + task);
+ }
+ return null;
+ }
+ task.getBounds(mTmpRect);
+ mTmpRect.offsetTo(0, 0);
+ final SurfaceControl.ScreenshotGraphicBuffer screenshotBuffer =
+ SurfaceControl.captureLayers(
+ task.getSurfaceControl().getHandle(), mTmpRect, scaleFraction);
+ final GraphicBuffer buffer = screenshotBuffer != null ? screenshotBuffer.getGraphicBuffer()
+ : null;
+ if (buffer == null || buffer.getWidth() <= 1 || buffer.getHeight() <= 1) {
+ return null;
+ }
+ return screenshotBuffer;
+ }
+
@Nullable private TaskSnapshot snapshotTask(Task task) {
if (!mService.mPolicy.isScreenOn()) {
if (DEBUG_SCREENSHOT) {
@@ -248,12 +271,6 @@
}
return null;
}
- if (task.getSurfaceControl() == null) {
- if (DEBUG_SCREENSHOT) {
- Slog.w(TAG_WM, "Failed to take screenshot. No surface control for " + task);
- }
- return null;
- }
final AppWindowToken appWindowToken = findAppTokenForSnapshot(task);
if (appWindowToken == null) {
@@ -271,8 +288,6 @@
final boolean isLowRamDevice = ActivityManager.isLowRamDeviceStatic();
final float scaleFraction = isLowRamDevice ? mPersister.getReducedScale() : 1f;
- task.getBounds(mTmpRect);
- mTmpRect.offsetTo(0, 0);
final WindowState mainWindow = appWindowToken.findMainWindow();
if (mainWindow == null) {
@@ -280,18 +295,17 @@
return null;
}
final SurfaceControl.ScreenshotGraphicBuffer screenshotBuffer =
- SurfaceControl.captureLayers(
- task.getSurfaceControl().getHandle(), mTmpRect, scaleFraction);
- final GraphicBuffer buffer = screenshotBuffer != null ? screenshotBuffer.getGraphicBuffer()
- : null;
- if (buffer == null || buffer.getWidth() <= 1 || buffer.getHeight() <= 1) {
+ createTaskSnapshot(task, scaleFraction);
+
+ if (screenshotBuffer == null) {
if (DEBUG_SCREENSHOT) {
Slog.w(TAG_WM, "Failed to take screenshot for " + task);
}
return null;
}
final boolean isWindowTranslucent = mainWindow.getAttrs().format != PixelFormat.OPAQUE;
- return new TaskSnapshot(appWindowToken.mActivityComponent, buffer,
+ return new TaskSnapshot(
+ appWindowToken.mActivityComponent, screenshotBuffer.getGraphicBuffer(),
screenshotBuffer.getColorSpace(), appWindowToken.getConfiguration().orientation,
getInsets(mainWindow), isLowRamDevice /* reduced */, scaleFraction /* scale */,
true /* isRealSnapshot */, task.getWindowingMode(), getSystemUiVisibility(task),
diff --git a/services/net/java/android/net/NetworkStackClient.java b/services/net/java/android/net/NetworkStackClient.java
index 6b5842f..09c9b6d 100644
--- a/services/net/java/android/net/NetworkStackClient.java
+++ b/services/net/java/android/net/NetworkStackClient.java
@@ -26,22 +26,27 @@
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 @@
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 @@
@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 @@
}
/**
+ * 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 @@
}
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 @@
@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 void init() {
log("Network stack init");
- mNetworkStackStartRequested = true;
+ mWasSystemServerInitialized = true;
}
/**
@@ -202,6 +257,13 @@
*/
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 @@
}
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 @@
}
}
- 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 @@
"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/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index 5175c1d..a125e31 100755
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -1973,26 +1973,6 @@
public static final String KEY_CARRIER_WIFI_STRING_ARRAY = "carrier_wifi_string_array";
/**
- * Base64 Encoding method the carrier will use for encoding encrypted IMSI and SSID.
- * The value set as below:
- * 2045 - RFC2045 (default value)
- * 4648 - RFC4648
- *
- * @hide
- */
- public static final String KEY_IMSI_ENCODING_METHOD_INT = "imsi_encoding_method_int";
-
- /**
- * Defines the sequence of sending an encrypted IMSI identity for EAP-SIM/AKA authentication.
- * The value set as below:
- * 1 - encrypted IMSI as EAP-RESPONSE/IDENTITY (default one).
- * 2 - anonymous as EAP-RESPONSE/IDENTITY -> encrypted IMSI as EAP-RESPONSE/AKA|SIM-IDENTITY.
- *
- * @hide
- */
- public static final String KEY_EAP_IDENTITY_SEQUENCE_INT = "imsi_eap_identity_sequence_int";
-
- /**
* Time delay (in ms) after which we show the notification to switch the preferred
* network.
* @hide
@@ -3265,8 +3245,6 @@
sDefaults.putBoolean(KEY_STK_DISABLE_LAUNCH_BROWSER_BOOL, false);
sDefaults.putBoolean(KEY_ALLOW_METERED_NETWORK_FOR_CERT_DOWNLOAD_BOOL, false);
sDefaults.putStringArray(KEY_CARRIER_WIFI_STRING_ARRAY, null);
- sDefaults.putInt(KEY_IMSI_ENCODING_METHOD_INT, 2045);
- sDefaults.putInt(KEY_EAP_IDENTITY_SEQUENCE_INT, 1);
sDefaults.putInt(KEY_PREF_NETWORK_NOTIFICATION_DELAY_INT, -1);
sDefaults.putInt(KEY_EMERGENCY_NOTIFICATION_DELAY_INT, -1);
sDefaults.putBoolean(KEY_ALLOW_USSD_REQUESTS_VIA_TELEPHONY_MANAGER_BOOL, true);
diff --git a/tests/PackageWatchdog/Android.bp b/tests/PackageWatchdog/Android.bp
index b079965..88d92c4 100644
--- a/tests/PackageWatchdog/Android.bp
+++ b/tests/PackageWatchdog/Android.bp
@@ -18,11 +18,18 @@
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 eb19361..9771637 100644
--- a/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java
+++ b/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java
@@ -27,6 +27,7 @@
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 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 @@
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 @@
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 1cf3d36..aec4055 100644
--- a/tests/RollbackTest/Android.bp
+++ b/tests/RollbackTest/Android.bp
@@ -95,7 +95,7 @@
":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 81629aa..a9e20cd 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.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 @@
* 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 @@
* @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 1ddfa6e..1a29c4c 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 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 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 @@
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 @@
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 75a95ad..bad2947 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.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 @@
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 @@
}
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");
+ }
}