Merge "video files with mimetype “video/x-ms-asf” can't scan to Gallery" 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/bluetooth/BluetoothProfileConnector.java b/core/java/android/bluetooth/BluetoothProfileConnector.java
index d9987249..863fd36 100644
--- a/core/java/android/bluetooth/BluetoothProfileConnector.java
+++ b/core/java/android/bluetooth/BluetoothProfileConnector.java
@@ -32,12 +32,12 @@
* @hide
*/
public abstract class BluetoothProfileConnector<T> {
- private int mProfileId;
+ private final int mProfileId;
private BluetoothProfile.ServiceListener mServiceListener;
- private BluetoothProfile mProfileProxy;
+ private final BluetoothProfile mProfileProxy;
private Context mContext;
- private String mProfileName;
- private String mServiceName;
+ private final String mProfileName;
+ private final String mServiceName;
private volatile T mService;
private final IBluetoothStateChangeCallback mBluetoothStateChangeCallback =
@@ -65,7 +65,7 @@
logDebug("Proxy object disconnected");
doUnbind();
if (mServiceListener != null) {
- mServiceListener.onServiceDisconnected(BluetoothProfile.A2DP);
+ mServiceListener.onServiceDisconnected(mProfileId);
}
}
};
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/android/net/DnsResolver.java b/core/java/android/net/DnsResolver.java
index 4b2b4c3..7a85dcb 100644
--- a/core/java/android/net/DnsResolver.java
+++ b/core/java/android/net/DnsResolver.java
@@ -16,7 +16,7 @@
package android.net;
-import static android.net.NetworkUtils.getDnsNetId;
+import static android.net.NetworkUtils.getDnsNetwork;
import static android.net.NetworkUtils.resNetworkCancel;
import static android.net.NetworkUtils.resNetworkQuery;
import static android.net.NetworkUtils.resNetworkResult;
@@ -333,7 +333,7 @@
final Object lock = new Object();
final Network queryNetwork;
try {
- queryNetwork = (network != null) ? network : new Network(getDnsNetId());
+ queryNetwork = (network != null) ? network : getDnsNetwork();
} catch (ErrnoException e) {
executor.execute(() -> callback.onError(new DnsException(ERROR_SYSTEM, e)));
return;
@@ -433,7 +433,7 @@
final FileDescriptor queryfd;
final Network queryNetwork;
try {
- queryNetwork = (network != null) ? network : new Network(getDnsNetId());
+ queryNetwork = (network != null) ? network : getDnsNetwork();
queryfd = resNetworkQuery(queryNetwork.getNetIdForResolv(), domain, CLASS_IN, nsType,
flags);
} catch (ErrnoException e) {
diff --git a/core/java/android/net/NetworkStats.java b/core/java/android/net/NetworkStats.java
index e892b65..bb344e2 100644
--- a/core/java/android/net/NetworkStats.java
+++ b/core/java/android/net/NetworkStats.java
@@ -18,6 +18,7 @@
import static android.os.Process.CLAT_UID;
+import android.annotation.NonNull;
import android.annotation.UnsupportedAppUsage;
import android.os.Parcel;
import android.os.Parcelable;
@@ -33,6 +34,7 @@
import java.io.CharArrayWriter;
import java.io.PrintWriter;
import java.util.Arrays;
+import java.util.function.Predicate;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
@@ -993,23 +995,33 @@
if (limitUid == UID_ALL && limitTag == TAG_ALL && limitIfaces == INTERFACES_ALL) {
return;
}
+ filter(e -> (limitUid == UID_ALL || limitUid == e.uid)
+ && (limitTag == TAG_ALL || limitTag == e.tag)
+ && (limitIfaces == INTERFACES_ALL
+ || ArrayUtils.contains(limitIfaces, e.iface)));
+ }
+ /**
+ * Only keep entries with {@link #set} value less than {@link #SET_DEBUG_START}.
+ *
+ * <p>This mutates the original structure in place.
+ */
+ public void filterDebugEntries() {
+ filter(e -> e.set < SET_DEBUG_START);
+ }
+
+ private void filter(Predicate<Entry> predicate) {
Entry entry = new Entry();
int nextOutputEntry = 0;
for (int i = 0; i < size; i++) {
entry = getValues(i, entry);
- final boolean matches =
- (limitUid == UID_ALL || limitUid == entry.uid)
- && (limitTag == TAG_ALL || limitTag == entry.tag)
- && (limitIfaces == INTERFACES_ALL
- || ArrayUtils.contains(limitIfaces, entry.iface));
-
- if (matches) {
- setValues(nextOutputEntry, entry);
+ if (predicate.test(entry)) {
+ if (nextOutputEntry != i) {
+ setValues(nextOutputEntry, entry);
+ }
nextOutputEntry++;
}
}
-
size = nextOutputEntry;
}
@@ -1175,133 +1187,221 @@
/**
* VPN accounting. Move some VPN's underlying traffic to other UIDs that use tun0 iface.
*
- * This method should only be called on delta NetworkStats. Do not call this method on a
- * snapshot {@link NetworkStats} object because the tunUid and/or the underlyingIface may
- * change over time.
+ * <p>This method should only be called on delta NetworkStats. Do not call this method on a
+ * snapshot {@link NetworkStats} object because the tunUid and/or the underlyingIface may change
+ * over time.
*
- * This method performs adjustments for one active VPN package and one VPN iface at a time.
- *
- * It is possible for the VPN software to use multiple underlying networks. This method
- * only migrates traffic for the primary underlying network.
+ * <p>This method performs adjustments for one active VPN package and one VPN iface at a time.
*
* @param tunUid uid of the VPN application
* @param tunIface iface of the vpn tunnel
- * @param underlyingIface the primary underlying network iface used by the VPN application
- * @return true if it successfully adjusts the accounting for VPN, false otherwise
+ * @param underlyingIfaces underlying network ifaces used by the VPN application
*/
- public boolean migrateTun(int tunUid, String tunIface, String underlyingIface) {
- Entry tunIfaceTotal = new Entry();
- Entry underlyingIfaceTotal = new Entry();
+ public void migrateTun(int tunUid, @NonNull String tunIface,
+ @NonNull String[] underlyingIfaces) {
+ // Combined usage by all apps using VPN.
+ final Entry tunIfaceTotal = new Entry();
+ // Usage by VPN, grouped by its {@code underlyingIfaces}.
+ final Entry[] perInterfaceTotal = new Entry[underlyingIfaces.length];
+ // Usage by VPN, summed across all its {@code underlyingIfaces}.
+ final Entry underlyingIfacesTotal = new Entry();
- tunAdjustmentInit(tunUid, tunIface, underlyingIface, tunIfaceTotal, underlyingIfaceTotal);
+ for (int i = 0; i < perInterfaceTotal.length; i++) {
+ perInterfaceTotal[i] = new Entry();
+ }
- // If tunIface < underlyingIface, it leaves the overhead traffic in the VPN app.
- // If tunIface > underlyingIface, the VPN app doesn't get credit for data compression.
+ tunAdjustmentInit(tunUid, tunIface, underlyingIfaces, tunIfaceTotal, perInterfaceTotal,
+ underlyingIfacesTotal);
+
+ // If tunIface < underlyingIfacesTotal, it leaves the overhead traffic in the VPN app.
+ // If tunIface > underlyingIfacesTotal, the VPN app doesn't get credit for data compression.
// Negative stats should be avoided.
- Entry pool = tunGetPool(tunIfaceTotal, underlyingIfaceTotal);
- if (pool.isEmpty()) {
- return true;
- }
- Entry moved =
- addTrafficToApplications(tunUid, tunIface, underlyingIface, tunIfaceTotal, pool);
- deductTrafficFromVpnApp(tunUid, underlyingIface, moved);
-
- if (!moved.isEmpty()) {
- Slog.wtf(TAG, "Failed to deduct underlying network traffic from VPN package. Moved="
- + moved);
- return false;
- }
- return true;
+ final Entry[] moved =
+ addTrafficToApplications(tunUid, tunIface, underlyingIfaces, tunIfaceTotal,
+ perInterfaceTotal, underlyingIfacesTotal);
+ deductTrafficFromVpnApp(tunUid, underlyingIfaces, moved);
}
/**
* Initializes the data used by the migrateTun() method.
*
- * This is the first pass iteration which does the following work:
- * (1) Adds up all the traffic through the tunUid's underlyingIface
- * (both foreground and background).
- * (2) Adds up all the traffic through tun0 excluding traffic from the vpn app itself.
+ * <p>This is the first pass iteration which does the following work:
+ *
+ * <ul>
+ * <li>Adds up all the traffic through the tunUid's underlyingIfaces (both foreground and
+ * background).
+ * <li>Adds up all the traffic through tun0 excluding traffic from the vpn app itself.
+ * </ul>
+ *
+ * @param tunUid uid of the VPN application
+ * @param tunIface iface of the vpn tunnel
+ * @param underlyingIfaces underlying network ifaces used by the VPN application
+ * @param tunIfaceTotal output parameter; combined data usage by all apps using VPN
+ * @param perInterfaceTotal output parameter; data usage by VPN app, grouped by its {@code
+ * underlyingIfaces}
+ * @param underlyingIfacesTotal output parameter; data usage by VPN, summed across all of its
+ * {@code underlyingIfaces}
*/
- private void tunAdjustmentInit(int tunUid, String tunIface, String underlyingIface,
- Entry tunIfaceTotal, Entry underlyingIfaceTotal) {
- Entry recycle = new Entry();
+ private void tunAdjustmentInit(int tunUid, @NonNull String tunIface,
+ @NonNull String[] underlyingIfaces, @NonNull Entry tunIfaceTotal,
+ @NonNull Entry[] perInterfaceTotal, @NonNull Entry underlyingIfacesTotal) {
+ final Entry recycle = new Entry();
for (int i = 0; i < size; i++) {
getValues(i, recycle);
if (recycle.uid == UID_ALL) {
throw new IllegalStateException(
"Cannot adjust VPN accounting on an iface aggregated NetworkStats.");
- } if (recycle.set == SET_DBG_VPN_IN || recycle.set == SET_DBG_VPN_OUT) {
+ }
+ if (recycle.set == SET_DBG_VPN_IN || recycle.set == SET_DBG_VPN_OUT) {
throw new IllegalStateException(
"Cannot adjust VPN accounting on a NetworkStats containing SET_DBG_VPN_*");
}
-
- if (recycle.uid == tunUid && recycle.tag == TAG_NONE
- && Objects.equals(underlyingIface, recycle.iface)) {
- underlyingIfaceTotal.add(recycle);
+ if (recycle.tag != TAG_NONE) {
+ // TODO(b/123666283): Take all tags for tunUid into account.
+ continue;
}
- if (recycle.uid != tunUid && recycle.tag == TAG_NONE
- && Objects.equals(tunIface, recycle.iface)) {
+ if (recycle.uid == tunUid) {
+ // Add up traffic through tunUid's underlying interfaces.
+ for (int j = 0; j < underlyingIfaces.length; j++) {
+ if (Objects.equals(underlyingIfaces[j], recycle.iface)) {
+ perInterfaceTotal[j].add(recycle);
+ underlyingIfacesTotal.add(recycle);
+ break;
+ }
+ }
+ } else if (tunIface.equals(recycle.iface)) {
// Add up all tunIface traffic excluding traffic from the vpn app itself.
tunIfaceTotal.add(recycle);
}
}
}
- private static Entry tunGetPool(Entry tunIfaceTotal, Entry underlyingIfaceTotal) {
- Entry pool = new Entry();
- pool.rxBytes = Math.min(tunIfaceTotal.rxBytes, underlyingIfaceTotal.rxBytes);
- pool.rxPackets = Math.min(tunIfaceTotal.rxPackets, underlyingIfaceTotal.rxPackets);
- pool.txBytes = Math.min(tunIfaceTotal.txBytes, underlyingIfaceTotal.txBytes);
- pool.txPackets = Math.min(tunIfaceTotal.txPackets, underlyingIfaceTotal.txPackets);
- pool.operations = Math.min(tunIfaceTotal.operations, underlyingIfaceTotal.operations);
- return pool;
- }
+ /**
+ * Distributes traffic across apps that are using given {@code tunIface}, and returns the total
+ * traffic that should be moved off of {@code tunUid} grouped by {@code underlyingIfaces}.
+ *
+ * @param tunUid uid of the VPN application
+ * @param tunIface iface of the vpn tunnel
+ * @param underlyingIfaces underlying network ifaces used by the VPN application
+ * @param tunIfaceTotal combined data usage across all apps using {@code tunIface}
+ * @param perInterfaceTotal data usage by VPN app, grouped by its {@code underlyingIfaces}
+ * @param underlyingIfacesTotal data usage by VPN, summed across all of its {@code
+ * underlyingIfaces}
+ */
+ private Entry[] addTrafficToApplications(int tunUid, @NonNull String tunIface,
+ @NonNull String[] underlyingIfaces, @NonNull Entry tunIfaceTotal,
+ @NonNull Entry[] perInterfaceTotal, @NonNull Entry underlyingIfacesTotal) {
+ // Traffic that should be moved off of each underlying interface for tunUid (see
+ // deductTrafficFromVpnApp below).
+ final Entry[] moved = new Entry[underlyingIfaces.length];
+ for (int i = 0; i < underlyingIfaces.length; i++) {
+ moved[i] = new Entry();
+ }
- private Entry addTrafficToApplications(int tunUid, String tunIface, String underlyingIface,
- Entry tunIfaceTotal, Entry pool) {
- Entry moved = new Entry();
- Entry tmpEntry = new Entry();
- tmpEntry.iface = underlyingIface;
- for (int i = 0; i < size; i++) {
- // the vpn app is excluded from the redistribution but all moved traffic will be
- // deducted from the vpn app (see deductTrafficFromVpnApp below).
- if (Objects.equals(iface[i], tunIface) && uid[i] != tunUid) {
- if (tunIfaceTotal.rxBytes > 0) {
- tmpEntry.rxBytes = pool.rxBytes * rxBytes[i] / tunIfaceTotal.rxBytes;
- } else {
- tmpEntry.rxBytes = 0;
- }
- if (tunIfaceTotal.rxPackets > 0) {
- tmpEntry.rxPackets = pool.rxPackets * rxPackets[i] / tunIfaceTotal.rxPackets;
- } else {
- tmpEntry.rxPackets = 0;
- }
- if (tunIfaceTotal.txBytes > 0) {
- tmpEntry.txBytes = pool.txBytes * txBytes[i] / tunIfaceTotal.txBytes;
- } else {
- tmpEntry.txBytes = 0;
- }
- if (tunIfaceTotal.txPackets > 0) {
- tmpEntry.txPackets = pool.txPackets * txPackets[i] / tunIfaceTotal.txPackets;
- } else {
- tmpEntry.txPackets = 0;
- }
- if (tunIfaceTotal.operations > 0) {
- tmpEntry.operations =
- pool.operations * operations[i] / tunIfaceTotal.operations;
- } else {
- tmpEntry.operations = 0;
- }
- tmpEntry.uid = uid[i];
- tmpEntry.tag = tag[i];
+ final Entry tmpEntry = new Entry();
+ final int origSize = size;
+ for (int i = 0; i < origSize; i++) {
+ if (!Objects.equals(iface[i], tunIface)) {
+ // Consider only entries that go onto the VPN interface.
+ continue;
+ }
+ if (uid[i] == tunUid) {
+ // Exclude VPN app from the redistribution, as it can choose to create packet
+ // streams by writing to itself.
+ continue;
+ }
+ tmpEntry.uid = uid[i];
+ tmpEntry.tag = tag[i];
+ tmpEntry.metered = metered[i];
+ tmpEntry.roaming = roaming[i];
+ tmpEntry.defaultNetwork = defaultNetwork[i];
+
+ // In a first pass, compute this entry's total share of data across all
+ // underlyingIfaces. This is computed on the basis of the share of this entry's usage
+ // over tunIface.
+ // TODO: Consider refactoring first pass into a separate helper method.
+ long totalRxBytes = 0;
+ if (tunIfaceTotal.rxBytes > 0) {
+ // Note - The multiplication below should not overflow since NetworkStatsService
+ // processes this every time device has transmitted/received amount equivalent to
+ // global threshold alert (~ 2MB) across all interfaces.
+ final long rxBytesAcrossUnderlyingIfaces =
+ underlyingIfacesTotal.rxBytes * rxBytes[i] / tunIfaceTotal.rxBytes;
+ // app must not be blamed for more than it consumed on tunIface
+ totalRxBytes = Math.min(rxBytes[i], rxBytesAcrossUnderlyingIfaces);
+ }
+ long totalRxPackets = 0;
+ if (tunIfaceTotal.rxPackets > 0) {
+ final long rxPacketsAcrossUnderlyingIfaces =
+ underlyingIfacesTotal.rxPackets * rxPackets[i] / tunIfaceTotal.rxPackets;
+ totalRxPackets = Math.min(rxPackets[i], rxPacketsAcrossUnderlyingIfaces);
+ }
+ long totalTxBytes = 0;
+ if (tunIfaceTotal.txBytes > 0) {
+ final long txBytesAcrossUnderlyingIfaces =
+ underlyingIfacesTotal.txBytes * txBytes[i] / tunIfaceTotal.txBytes;
+ totalTxBytes = Math.min(txBytes[i], txBytesAcrossUnderlyingIfaces);
+ }
+ long totalTxPackets = 0;
+ if (tunIfaceTotal.txPackets > 0) {
+ final long txPacketsAcrossUnderlyingIfaces =
+ underlyingIfacesTotal.txPackets * txPackets[i] / tunIfaceTotal.txPackets;
+ totalTxPackets = Math.min(txPackets[i], txPacketsAcrossUnderlyingIfaces);
+ }
+ long totalOperations = 0;
+ if (tunIfaceTotal.operations > 0) {
+ final long operationsAcrossUnderlyingIfaces =
+ underlyingIfacesTotal.operations * operations[i] / tunIfaceTotal.operations;
+ totalOperations = Math.min(operations[i], operationsAcrossUnderlyingIfaces);
+ }
+ // In a second pass, distribute these values across interfaces in the proportion that
+ // each interface represents of the total traffic of the underlying interfaces.
+ for (int j = 0; j < underlyingIfaces.length; j++) {
+ tmpEntry.iface = underlyingIfaces[j];
+ tmpEntry.rxBytes = 0;
+ // Reset 'set' to correct value since it gets updated when adding debug info below.
tmpEntry.set = set[i];
- tmpEntry.metered = metered[i];
- tmpEntry.roaming = roaming[i];
- tmpEntry.defaultNetwork = defaultNetwork[i];
+ if (underlyingIfacesTotal.rxBytes > 0) {
+ tmpEntry.rxBytes =
+ totalRxBytes
+ * perInterfaceTotal[j].rxBytes
+ / underlyingIfacesTotal.rxBytes;
+ }
+ tmpEntry.rxPackets = 0;
+ if (underlyingIfacesTotal.rxPackets > 0) {
+ tmpEntry.rxPackets =
+ totalRxPackets
+ * perInterfaceTotal[j].rxPackets
+ / underlyingIfacesTotal.rxPackets;
+ }
+ tmpEntry.txBytes = 0;
+ if (underlyingIfacesTotal.txBytes > 0) {
+ tmpEntry.txBytes =
+ totalTxBytes
+ * perInterfaceTotal[j].txBytes
+ / underlyingIfacesTotal.txBytes;
+ }
+ tmpEntry.txPackets = 0;
+ if (underlyingIfacesTotal.txPackets > 0) {
+ tmpEntry.txPackets =
+ totalTxPackets
+ * perInterfaceTotal[j].txPackets
+ / underlyingIfacesTotal.txPackets;
+ }
+ tmpEntry.operations = 0;
+ if (underlyingIfacesTotal.operations > 0) {
+ tmpEntry.operations =
+ totalOperations
+ * perInterfaceTotal[j].operations
+ / underlyingIfacesTotal.operations;
+ }
+ // tmpEntry now contains the migrated data of the i-th entry for the j-th underlying
+ // interface. Add that data usage to this object.
combineValues(tmpEntry);
if (tag[i] == TAG_NONE) {
- moved.add(tmpEntry);
+ // Add the migrated data to moved so it is deducted from the VPN app later.
+ moved[j].add(tmpEntry);
// Add debug info
tmpEntry.set = SET_DBG_VPN_IN;
combineValues(tmpEntry);
@@ -1311,38 +1411,45 @@
return moved;
}
- private void deductTrafficFromVpnApp(int tunUid, String underlyingIface, Entry moved) {
- // Add debug info
- moved.uid = tunUid;
- moved.set = SET_DBG_VPN_OUT;
- moved.tag = TAG_NONE;
- moved.iface = underlyingIface;
- moved.metered = METERED_ALL;
- moved.roaming = ROAMING_ALL;
- moved.defaultNetwork = DEFAULT_NETWORK_ALL;
- combineValues(moved);
+ private void deductTrafficFromVpnApp(
+ int tunUid,
+ @NonNull String[] underlyingIfaces,
+ @NonNull Entry[] moved) {
+ for (int i = 0; i < underlyingIfaces.length; i++) {
+ moved[i].uid = tunUid;
+ // Add debug info
+ moved[i].set = SET_DBG_VPN_OUT;
+ moved[i].tag = TAG_NONE;
+ moved[i].iface = underlyingIfaces[i];
+ moved[i].metered = METERED_ALL;
+ moved[i].roaming = ROAMING_ALL;
+ moved[i].defaultNetwork = DEFAULT_NETWORK_ALL;
+ combineValues(moved[i]);
- // Caveat: if the vpn software uses tag, the total tagged traffic may be greater than
- // the TAG_NONE traffic.
- //
- // Relies on the fact that the underlying traffic only has state ROAMING_NO and METERED_NO,
- // which should be the case as it comes directly from the /proc file. We only blend in the
- // roaming data after applying these adjustments, by checking the NetworkIdentity of the
- // underlying iface.
- int idxVpnBackground = findIndex(underlyingIface, tunUid, SET_DEFAULT, TAG_NONE,
- METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO);
- if (idxVpnBackground != -1) {
- tunSubtract(idxVpnBackground, this, moved);
- }
+ // Caveat: if the vpn software uses tag, the total tagged traffic may be greater than
+ // the TAG_NONE traffic.
+ //
+ // Relies on the fact that the underlying traffic only has state ROAMING_NO and
+ // METERED_NO, which should be the case as it comes directly from the /proc file.
+ // We only blend in the roaming data after applying these adjustments, by checking the
+ // NetworkIdentity of the underlying iface.
+ final int idxVpnBackground = findIndex(underlyingIfaces[i], tunUid, SET_DEFAULT,
+ TAG_NONE, METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO);
+ if (idxVpnBackground != -1) {
+ // Note - tunSubtract also updates moved[i]; whatever traffic that's left is removed
+ // from foreground usage.
+ tunSubtract(idxVpnBackground, this, moved[i]);
+ }
- int idxVpnForeground = findIndex(underlyingIface, tunUid, SET_FOREGROUND, TAG_NONE,
- METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO);
- if (idxVpnForeground != -1) {
- tunSubtract(idxVpnForeground, this, moved);
+ final int idxVpnForeground = findIndex(underlyingIfaces[i], tunUid, SET_FOREGROUND,
+ TAG_NONE, METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO);
+ if (idxVpnForeground != -1) {
+ tunSubtract(idxVpnForeground, this, moved[i]);
+ }
}
}
- private static void tunSubtract(int i, NetworkStats left, Entry right) {
+ private static void tunSubtract(int i, @NonNull NetworkStats left, @NonNull Entry right) {
long rxBytes = Math.min(left.rxBytes[i], right.rxBytes);
left.rxBytes[i] -= rxBytes;
right.rxBytes -= rxBytes;
diff --git a/core/java/android/net/NetworkUtils.java b/core/java/android/net/NetworkUtils.java
index a640f83..d0f54b4 100644
--- a/core/java/android/net/NetworkUtils.java
+++ b/core/java/android/net/NetworkUtils.java
@@ -158,10 +158,9 @@
/**
* DNS resolver series jni method.
- * Attempts to get netid of network which resolver will
- * use if no network is explicitly selected.
+ * Attempts to get network which resolver will use if no network is explicitly selected.
*/
- public static native int getDnsNetId() throws ErrnoException;
+ public static native Network getDnsNetwork() throws ErrnoException;
/**
* Get the tcp repair window associated with the {@code fd}.
diff --git a/core/java/android/os/Build.java b/core/java/android/os/Build.java
index deb9eba..77d367f 100755
--- a/core/java/android/os/Build.java
+++ b/core/java/android/os/Build.java
@@ -113,8 +113,7 @@
/**
* A hardware serial number, if available. Alphanumeric only, case-insensitive.
- * For apps targeting SDK higher than {@link Build.VERSION_CODES#O_MR1} this
- * field is set to {@link Build#UNKNOWN}.
+ * This field is always set to {@link Build#UNKNOWN}.
*
* @deprecated Use {@link #getSerial()} instead.
**/
diff --git a/core/java/android/os/GraphicsEnvironment.java b/core/java/android/os/GraphicsEnvironment.java
index 5039b31..24a1477 100644
--- a/core/java/android/os/GraphicsEnvironment.java
+++ b/core/java/android/os/GraphicsEnvironment.java
@@ -231,13 +231,6 @@
}
/**
- * Check whether application is profileable
- */
- private static boolean isProfileable(Context context) {
- return context.getApplicationInfo().isProfileableByShell();
- }
-
- /**
* Store the layer paths available to the loader.
*/
public void setLayerPaths(ClassLoader classLoader,
@@ -287,11 +280,11 @@
String layerPaths = "";
// Only enable additional debug functionality if the following conditions are met:
- // 1. App is debuggable, profileable, or device is rooted
+ // 1. App is debuggable or device is rooted
// 2. ENABLE_GPU_DEBUG_LAYERS is true
// 3. Package name is equal to GPU_DEBUG_APP
- if (isDebuggable(context) || isProfileable(context) || (getCanLoadSystemLibraries() == 1)) {
+ if (isDebuggable(context) || (getCanLoadSystemLibraries() == 1)) {
final int enable = coreSettings.getInt(Settings.Global.ENABLE_GPU_DEBUG_LAYERS, 0);
@@ -473,9 +466,8 @@
*/
private String getAngleDebugPackage(Context context, Bundle coreSettings) {
final boolean appIsDebuggable = isDebuggable(context);
- final boolean appIsProfileable = isProfileable(context);
final boolean deviceIsDebuggable = getCanLoadSystemLibraries() == 1;
- if (appIsDebuggable || appIsProfileable || deviceIsDebuggable) {
+ if (appIsDebuggable || deviceIsDebuggable) {
String debugPackage;
if (coreSettings != null) {
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 66b9d168..0db5c36 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -8081,6 +8081,15 @@
"lock_screen_show_silent_notifications";
/**
+ * Indicates whether snooze options should be shown on notifications
+ * <p>
+ * Type: int (0 for false, 1 for true)
+ *
+ * @hide
+ */
+ public static final String SHOW_NOTIFICATION_SNOOZE = "show_notification_snooze";
+
+ /**
* List of TV inputs that are currently hidden. This is a string
* containing the IDs of all hidden TV inputs. Each ID is encoded by
* {@link android.net.Uri#encode(String)} and separated by ':'.
@@ -8968,6 +8977,7 @@
LOCK_SCREEN_CUSTOM_CLOCK_FACE,
LOCK_SCREEN_SHOW_NOTIFICATIONS,
LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS,
+ SHOW_NOTIFICATION_SNOOZE,
ZEN_DURATION,
SHOW_ZEN_UPGRADE_NOTIFICATION,
SHOW_ZEN_SETTINGS_SUGGESTION,
@@ -9150,6 +9160,7 @@
VALIDATORS.put(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, BOOLEAN_VALIDATOR);
VALIDATORS.put(LOCK_SCREEN_SHOW_NOTIFICATIONS, BOOLEAN_VALIDATOR);
VALIDATORS.put(LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS, BOOLEAN_VALIDATOR);
+ VALIDATORS.put(SHOW_NOTIFICATION_SNOOZE, BOOLEAN_VALIDATOR);
VALIDATORS.put(ZEN_DURATION, ZEN_DURATION_VALIDATOR);
VALIDATORS.put(SHOW_ZEN_UPGRADE_NOTIFICATION, BOOLEAN_VALIDATOR);
VALIDATORS.put(SHOW_ZEN_SETTINGS_SUGGESTION, BOOLEAN_VALIDATOR);
diff --git a/core/java/android/view/Window.java b/core/java/android/view/Window.java
index 9436633..73e0e4b 100644
--- a/core/java/android/view/Window.java
+++ b/core/java/android/view/Window.java
@@ -1301,7 +1301,7 @@
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
final boolean isOutside =
- event.getAction() == MotionEvent.ACTION_DOWN && isOutOfBounds(context, event)
+ event.getAction() == MotionEvent.ACTION_UP && isOutOfBounds(context, event)
|| event.getAction() == MotionEvent.ACTION_OUTSIDE;
if (mCloseOnTouchOutside && peekDecorView() != null && isOutside) {
return true;
diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java
index 7eb3dab..ee99837 100644
--- a/core/java/com/android/internal/app/ChooserActivity.java
+++ b/core/java/com/android/internal/app/ChooserActivity.java
@@ -1434,6 +1434,22 @@
List<ShortcutManager.ShareShortcutInfo> resultList,
List<DisplayResolveInfo> driList,
@Nullable List<AppTarget> appTargets) {
+ if (appTargets != null && appTargets.size() != resultList.size()) {
+ throw new RuntimeException("resultList and appTargets must have the same size."
+ + " resultList.size()=" + resultList.size()
+ + " appTargets.size()=" + appTargets.size());
+ }
+
+ for (int i = resultList.size() - 1; i >= 0; i--) {
+ final String packageName = resultList.get(i).getTargetComponent().getPackageName();
+ if (!isPackageEnabled(packageName)) {
+ resultList.remove(i);
+ if (appTargets != null) {
+ appTargets.remove(i);
+ }
+ }
+ }
+
// Match ShareShortcutInfos with DisplayResolveInfos to be able to use the old code path
// for direct share targets. After ShareSheet is refactored we should use the
// ShareShortcutInfos directly.
@@ -1447,7 +1463,6 @@
ChooserTarget chooserTarget = convertToChooserTarget(shareShortcutInfo);
chooserTargets.add(chooserTarget);
if (mDirectShareAppTargetCache != null && appTargets != null) {
- // Note that appTargets.size() == resultList.size() is always true.
mDirectShareAppTargetCache.put(chooserTarget, appTargets.get(j));
}
}
@@ -1473,6 +1488,24 @@
mChooserHandler.sendMessage(msg);
}
+ private boolean isPackageEnabled(String packageName) {
+ if (TextUtils.isEmpty(packageName)) {
+ return false;
+ }
+ ApplicationInfo appInfo;
+ try {
+ appInfo = getPackageManager().getApplicationInfo(packageName, 0);
+ } catch (NameNotFoundException e) {
+ return false;
+ }
+
+ if (appInfo != null && appInfo.enabled
+ && (appInfo.flags & ApplicationInfo.FLAG_SUSPENDED) == 0) {
+ return true;
+ }
+ return false;
+ }
+
private ChooserTarget convertToChooserTarget(ShortcutManager.ShareShortcutInfo shareShortcut) {
ShortcutInfo shortcutInfo = shareShortcut.getShortcutInfo();
Bundle extras = new Bundle();
diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java
index 538c81d..0a01beb 100644
--- a/core/java/com/android/internal/app/ResolverActivity.java
+++ b/core/java/com/android/internal/app/ResolverActivity.java
@@ -383,9 +383,11 @@
mSystemWindowInsets.right, 0);
View emptyView = findViewById(R.id.empty);
- emptyView.setPadding(0, 0, 0, mSystemWindowInsets.bottom
- + getResources().getDimensionPixelSize(
- R.dimen.chooser_edge_margin_normal) * 2);
+ if (emptyView != null) {
+ emptyView.setPadding(0, 0, 0, mSystemWindowInsets.bottom
+ + getResources().getDimensionPixelSize(
+ R.dimen.chooser_edge_margin_normal) * 2);
+ }
if (mFooterSpacer == null) {
mFooterSpacer = new Space(getApplicationContext());
diff --git a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java
index 14fe6ab..1b0a458 100644
--- a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java
+++ b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java
@@ -113,5 +113,37 @@
*/
public static final String PROPERTY_PERMISSIONS_HUB_ENABLED = "permissions_hub_enabled";
+ // Flags related to Assistant Handles
+
+ /**
+ * (String) Which behavior mode for the Assistant Handles to use.
+ */
+ public static final String ASSIST_HANDLES_BEHAVIOR_MODE = "assist_handles_behavior_mode";
+
+ /**
+ * (long) How long, in milliseconds, to display Assist Handles when showing them temporarily.
+ */
+ public static final String ASSIST_HANDLES_SHOW_AND_GO_DURATION_MS =
+ "assist_handles_show_and_go_duration_ms";
+
+ /**
+ * (long) How long, in milliseconds, to wait before displaying Assist Handles temporarily after
+ * hiding them.
+ */
+ public static final String ASSIST_HANDLES_SHOWN_FREQUENCY_THRESHOLD_MS =
+ "assist_handles_shown_frequency_threshold_ms";
+
+ /**
+ * (long) How long, in milliseconds, for teaching behaviors to wait before considering the user
+ * taught.
+ */
+ public static final String ASSIST_HANDLES_LEARN_TIME_MS = "assist_handles_learn_time_ms";
+
+ /**
+ * (int) How many times for teaching behaviors to see the user perform an action to consider it
+ * taught.
+ */
+ public static final String ASSIST_HANDLES_LEARN_COUNT = "assist_handles_learn_count";
+
private SystemUiDeviceConfigFlags() { }
}
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/java/com/android/internal/net/VpnInfo.java b/core/java/com/android/internal/net/VpnInfo.java
index b1a41287..e74af5e 100644
--- a/core/java/com/android/internal/net/VpnInfo.java
+++ b/core/java/com/android/internal/net/VpnInfo.java
@@ -19,6 +19,8 @@
import android.os.Parcel;
import android.os.Parcelable;
+import java.util.Arrays;
+
/**
* A lightweight container used to carry information of the ongoing VPN.
* Internal use only..
@@ -28,14 +30,14 @@
public class VpnInfo implements Parcelable {
public int ownerUid;
public String vpnIface;
- public String primaryUnderlyingIface;
+ public String[] underlyingIfaces;
@Override
public String toString() {
return "VpnInfo{"
+ "ownerUid=" + ownerUid
+ ", vpnIface='" + vpnIface + '\''
- + ", primaryUnderlyingIface='" + primaryUnderlyingIface + '\''
+ + ", underlyingIfaces='" + Arrays.toString(underlyingIfaces) + '\''
+ '}';
}
@@ -48,7 +50,7 @@
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(ownerUid);
dest.writeString(vpnIface);
- dest.writeString(primaryUnderlyingIface);
+ dest.writeStringArray(underlyingIfaces);
}
public static final Parcelable.Creator<VpnInfo> CREATOR = new Parcelable.Creator<VpnInfo>() {
@@ -57,7 +59,7 @@
VpnInfo info = new VpnInfo();
info.ownerUid = source.readInt();
info.vpnIface = source.readString();
- info.primaryUnderlyingIface = source.readString();
+ info.underlyingIfaces = source.readStringArray();
return info;
}
diff --git a/core/jni/android_net_NetUtils.cpp b/core/jni/android_net_NetUtils.cpp
index 00e0e3a..08aa1d9 100644
--- a/core/jni/android_net_NetUtils.cpp
+++ b/core/jni/android_net_NetUtils.cpp
@@ -304,13 +304,19 @@
jniSetFileDescriptorOfFD(env, javaFd, -1);
}
-static jint android_net_utils_getDnsNetId(JNIEnv *env, jobject thiz) {
- int dnsNetId = getNetworkForDns();
- if (dnsNetId < 0) {
- throwErrnoException(env, "getDnsNetId", -dnsNetId);
+static jobject android_net_utils_getDnsNetwork(JNIEnv *env, jobject thiz) {
+ unsigned dnsNetId = 0;
+ if (int res = getNetworkForDns(&dnsNetId) < 0) {
+ throwErrnoException(env, "getDnsNetId", -res);
+ return nullptr;
}
+ bool privateDnsBypass = dnsNetId & NETID_USE_LOCAL_NAMESERVERS;
- return dnsNetId;
+ static jclass class_Network = MakeGlobalRefOrDie(
+ env, FindClassOrDie(env, "android/net/Network"));
+ static jmethodID ctor = env->GetMethodID(class_Network, "<init>", "(IZ)V");
+ return env->NewObject(
+ class_Network, ctor, dnsNetId & ~NETID_USE_LOCAL_NAMESERVERS, privateDnsBypass);
}
static jobject android_net_utils_getTcpRepairWindow(JNIEnv *env, jobject thiz, jobject javaFd) {
@@ -369,7 +375,7 @@
{ "resNetworkQuery", "(ILjava/lang/String;III)Ljava/io/FileDescriptor;", (void*) android_net_utils_resNetworkQuery },
{ "resNetworkResult", "(Ljava/io/FileDescriptor;)Landroid/net/DnsResolver$DnsResponse;", (void*) android_net_utils_resNetworkResult },
{ "resNetworkCancel", "(Ljava/io/FileDescriptor;)V", (void*) android_net_utils_resNetworkCancel },
- { "getDnsNetId", "()I", (void*) android_net_utils_getDnsNetId },
+ { "getDnsNetwork", "()Landroid/net/Network;", (void*) android_net_utils_getDnsNetwork },
};
int register_android_net_NetworkUtils(JNIEnv* env)
diff --git a/core/proto/android/server/jobscheduler.proto b/core/proto/android/server/jobscheduler.proto
index 3f8ddff..2873379 100644
--- a/core/proto/android/server/jobscheduler.proto
+++ b/core/proto/android/server/jobscheduler.proto
@@ -273,9 +273,11 @@
// The maximum number of jobs an app can run within this particular standby bucket's
// window size.
optional int32 max_job_count_rare = 11;
+ // The period of time used to rate limit recently run jobs.
+ optional int32 rate_limiting_window_ms = 19;
// The maximum number of jobs that should be allowed to run in the past
- // {@link QUOTA_CONTROLLER_ALLOWED_TIME_PER_PERIOD_MS}.
- optional int32 max_job_count_per_allowed_time = 12;
+ // rate_limiting_window_ms.
+ optional int32 max_job_count_per_rate_limiting_window = 12;
// The maximum number of timing sessions an app can run within this particular standby
// bucket's window size.
optional int32 max_session_count_active = 13;
@@ -289,8 +291,8 @@
// bucket's window size.
optional int32 max_session_count_rare = 16;
// The maximum number of timing sessions that should be allowed to run in the past
- // {@link QUOTA_CONTROLLER_ALLOWED_TIME_PER_PERIOD_MS}.
- optional int32 max_session_count_per_allowed_time = 17;
+ // rate_limiting_window_ms.
+ optional int32 max_session_count_per_rate_limiting_window = 17;
// Treat two distinct {@link TimingSession}s as the same if they start and end within this
// amount of time of each other.
optional int64 timing_session_coalescing_duration_ms = 18;
@@ -517,63 +519,56 @@
optional int64 expiration_time_elapsed = 2;
optional int64 window_size_ms = 3;
- /** The total amount of time the app ran in its respective bucket window size. */
+ optional int32 job_count_limit = 14;
+ optional int32 session_count_limit = 15;
+
+ // The total amount of time the app ran in its respective bucket window size.
optional int64 execution_time_in_window_ms = 4;
optional int32 bg_job_count_in_window = 5;
- /**
- * The total amount of time the app ran in the last
- * {@link QuotaController#MAX_PERIOD_MS}.
- */
+ // The total amount of time the app ran in the last
+ // {@link QuotaController#MAX_PERIOD_MS}.
optional int64 execution_time_in_max_period_ms = 6;
optional int32 bg_job_count_in_max_period = 7;
- /**
- * The number of {@link TimingSession}s within the bucket window size. This will include
- * sessions that started before the window as long as they end within the window.
- */
+ // The number of {@link TimingSession}s within the bucket window size. This will include
+ // sessions that started before the window as long as they end within the window.
optional int32 session_count_in_window = 11;
- /**
- * The time after which the sum of all the app's sessions plus
- * ConstantsProto.QuotaController.in_quota_buffer_ms equals the quota. This is only
- * valid if
- * execution_time_in_window_ms >=
- * ConstantsProto.QuotaController.allowed_time_per_period_ms
- * or
- * execution_time_in_max_period_ms >=
- * ConstantsProto.QuotaController.max_execution_time_ms.
- */
- optional int64 quota_cutoff_time_elapsed = 8;
+ // The time after which the app will be under the bucket quota. This is only valid if
+ // execution_time_in_window_ms >=
+ // ConstantsProto.QuotaController.allowed_time_per_period_ms
+ // or
+ // execution_time_in_max_period_ms >=
+ // ConstantsProto.QuotaController.max_execution_time_ms
+ // or
+ // bg_job_count_in_window >= job_count_limit
+ // or
+ // session_count_in_window >= session_count_limit.
+ optional int64 in_quota_time_elapsed = 8;
- /**
- * The time after which job_count_in_allowed_time should be considered invalid, in the
- * elapsed realtime timebase.
- */
+ // The time after which job_count_in_rate_limiting_window should be considered invalid,
+ // in the elapsed realtime timebase.
optional int64 job_count_expiration_time_elapsed = 9;
- /**
- * The number of jobs that ran in at least the last
- * ConstantsProto.QuotaController.allowed_time_per_period_ms.
- * It may contain a few stale entries since cleanup won't happen exactly every
- * ConstantsProto.QuotaController.allowed_time_per_period_ms.
- */
- optional int32 job_count_in_allowed_time = 10;
+ // The number of jobs that ran in at least the last
+ // ConstantsProto.QuotaController.rate_limiting_window_ms.
+ // It may contain a few stale entries since cleanup won't happen exactly every
+ // ConstantsProto.QuotaController.rate_limiting_window_ms. This should only be
+ // considered valid before elapsed realtime has reached
+ // job_count_expiration_time_elapsed.
+ optional int32 job_count_in_rate_limiting_window = 10;
- /**
- * The time after which {@link #timingSessionCountInAllowedTime} should be considered
- * invalid, in the elapsed realtime timebase.
- */
+ // The time after which {@link #timingSessionCountInAllowedTime} should be considered
+ // invalid, in the elapsed realtime timebase.
optional int64 session_count_expiration_time_elapsed = 12;
- /**
- * The number of {@link TimingSession}s that ran in at least the last
- * {@link #mAllowedTimePerPeriodMs}. It may contain a few stale entries since cleanup won't
- * happen exactly every {@link #mAllowedTimePerPeriodMs}. This should only be considered
- * valid before elapsed realtime has reached
- * {@link #timingSessionCountExpirationTimeElapsed}.
- */
- optional int32 session_count_in_allowed_time = 13;
+ // The number of {@link TimingSession}s that ran in at least the last
+ // ConstantsProto.QuotaController.rate_limiting_window_ms. It may contain a few stale
+ // entries since cleanup won't happen exactly every
+ // ConstantsProto.QuotaController.rate_limiting_window_ms. This should only be considered
+ // valid before elapsed realtime has reached session_count_expiration_time_elapsed.
+ optional int32 session_count_in_rate_limiting_window = 13;
}
message Package {
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/AndroidManifest.xml b/core/res/AndroidManifest.xml
index ab7aed3..890ad5e 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -2337,7 +2337,7 @@
The app can check whether it has this authorization by calling
{@link android.provider.Settings#canDrawOverlays
Settings.canDrawOverlays()}.
- <p>Protection level: signature -->
+ <p>Protection level: signature|preinstalled|appop|pre23|development -->
<permission android:name="android.permission.SYSTEM_ALERT_WINDOW"
android:label="@string/permlab_systemAlertWindow"
android:description="@string/permdesc_systemAlertWindow"
@@ -2522,7 +2522,7 @@
can check whether it has this authorization by calling {@link
android.provider.Settings.System#canWrite Settings.System.canWrite()}.
- <p>Protection level: signature
+ <p>Protection level: signature|preinstalled|appop|pre23
-->
<permission android:name="android.permission.WRITE_SETTINGS"
android:label="@string/permlab_writeSettings"
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/dimens.xml b/core/res/res/values/dimens.xml
index e0ab6c8..5363ef9 100644
--- a/core/res/res/values/dimens.xml
+++ b/core/res/res/values/dimens.xml
@@ -59,9 +59,7 @@
<!-- Height of the bottom navigation bar frame in landscape -->
<dimen name="navigation_bar_frame_height_landscape">@dimen/navigation_bar_frame_height</dimen>
- <!-- The height of the navigation gesture area; if the size is larger than the navigation bar
- frame width/height, then the difference is the spacing from the navigation bar window to
- the area that detects gestures. -->
+ <!-- The height of the navigation gesture area if the gesture is starting from the bottom. -->
<dimen name="navigation_bar_gesture_height">@dimen/navigation_bar_frame_height</dimen>
<!-- Height of the bottom navigation / system bar in car mode. -->
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/core/res/res/values/themes_device_defaults.xml b/core/res/res/values/themes_device_defaults.xml
index b901048..6289262 100644
--- a/core/res/res/values/themes_device_defaults.xml
+++ b/core/res/res/values/themes_device_defaults.xml
@@ -1473,14 +1473,10 @@
<item name="colorError">@color/error_color_device_default_light</item>
<item name="colorEdgeEffect">@color/edge_effect_device_default_light</item>
- <!-- Add divider that matches material -->
+ <!-- Add white nav bar with divider that matches material -->
<item name="navigationBarDividerColor">@color/navigation_bar_divider_device_default_settings</item>
-
- <!-- Add transparent nav and status bars with light icons to support drawing edge-to-edge
- for Q gestural navigation-->
- <item name="navigationBarColor">@android:color/transparent</item>
+ <item name="navigationBarColor">@android:color/white</item>
<item name="windowLightNavigationBar">true</item>
- <item name="statusBarColor">@android:color/transparent</item>
<!-- Dialog attributes -->
<item name="dialogCornerRadius">@dimen/config_dialogCornerRadius</item>
diff --git a/core/tests/benchmarks/src/android/net/NetworkStatsBenchmark.java b/core/tests/benchmarks/src/android/net/NetworkStatsBenchmark.java
index 1b65603..707d7b3 100644
--- a/core/tests/benchmarks/src/android/net/NetworkStatsBenchmark.java
+++ b/core/tests/benchmarks/src/android/net/NetworkStatsBenchmark.java
@@ -19,13 +19,22 @@
import com.google.caliper.BeforeExperiment;
import com.google.caliper.Param;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
public class NetworkStatsBenchmark {
- private static final String UNDERLYING_IFACE = "wlan0";
+ private static final String[] UNDERLYING_IFACES = {"wlan0", "rmnet0"};
private static final String TUN_IFACE = "tun0";
private static final int TUN_UID = 999999999;
@Param({"100", "1000"})
private int mSize;
+ /**
+ * Should not be more than the length of {@link #UNDERLYING_IFACES}.
+ */
+ @Param({"1", "2"})
+ private int mNumUnderlyingIfaces;
private NetworkStats mNetworkStats;
@BeforeExperiment
@@ -33,8 +42,10 @@
mNetworkStats = new NetworkStats(0, mSize + 2);
int uid = 0;
NetworkStats.Entry recycle = new NetworkStats.Entry();
+ final List<String> allIfaces = getAllIfacesForBenchmark(); // also contains TUN_IFACE.
+ final int totalIfaces = allIfaces.size();
for (int i = 0; i < mSize; i++) {
- recycle.iface = (i < mSize / 2) ? TUN_IFACE : UNDERLYING_IFACE;
+ recycle.iface = allIfaces.get(i % totalIfaces);
recycle.uid = uid;
recycle.set = i % 2;
recycle.tag = NetworkStats.TAG_NONE;
@@ -48,22 +59,39 @@
uid++;
}
}
- recycle.iface = UNDERLYING_IFACE;
- recycle.uid = TUN_UID;
- recycle.set = NetworkStats.SET_FOREGROUND;
- recycle.tag = NetworkStats.TAG_NONE;
- recycle.rxBytes = 90000 * mSize;
- recycle.rxPackets = 40 * mSize;
- recycle.txBytes = 180000 * mSize;
- recycle.txPackets = 1200 * mSize;
- recycle.operations = 0;
- mNetworkStats.addValues(recycle);
+
+ for (int i = 0; i < mNumUnderlyingIfaces; i++) {
+ recycle.iface = UNDERLYING_IFACES[i];
+ recycle.uid = TUN_UID;
+ recycle.set = NetworkStats.SET_FOREGROUND;
+ recycle.tag = NetworkStats.TAG_NONE;
+ recycle.rxBytes = 90000 * mSize;
+ recycle.rxPackets = 40 * mSize;
+ recycle.txBytes = 180000 * mSize;
+ recycle.txPackets = 1200 * mSize;
+ recycle.operations = 0;
+ mNetworkStats.addValues(recycle);
+ }
+ }
+
+ private String[] getVpnUnderlyingIfaces() {
+ return Arrays.copyOf(UNDERLYING_IFACES, mNumUnderlyingIfaces);
+ }
+
+ /**
+ * Same as {@link #getVpnUnderlyingIfaces}, but also contains {@link #TUN_IFACE}.
+ */
+ private List<String> getAllIfacesForBenchmark() {
+ List<String> ifaces = new ArrayList<>();
+ ifaces.add(TUN_IFACE);
+ ifaces.addAll(Arrays.asList(getVpnUnderlyingIfaces()));
+ return ifaces;
}
public void timeMigrateTun(int reps) {
for (int i = 0; i < reps; i++) {
NetworkStats stats = mNetworkStats.clone();
- stats.migrateTun(TUN_UID, TUN_IFACE, UNDERLYING_IFACE);
+ stats.migrateTun(TUN_UID, TUN_IFACE, getVpnUnderlyingIfaces());
}
}
diff --git a/data/etc/car/Android.bp b/data/etc/car/Android.bp
index 37020fc..9272ea5 100644
--- a/data/etc/car/Android.bp
+++ b/data/etc/car/Android.bp
@@ -31,6 +31,13 @@
}
prebuilt_etc {
+ name: "privapp_whitelist_android.car.cluster",
+ sub_dir: "permissions",
+ src: "android.car.cluster.xml",
+ filename_from_src: true,
+}
+
+prebuilt_etc {
name: "privapp_whitelist_android.car.usb.handler",
sub_dir: "permissions",
src: "android.car.usb.handler.xml",
diff --git a/data/etc/car/android.car.cluster.xml b/data/etc/car/android.car.cluster.xml
new file mode 100644
index 0000000..d7f29da
--- /dev/null
+++ b/data/etc/car/android.car.cluster.xml
@@ -0,0 +1,24 @@
+<?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
+ -->
+<permissions>
+ <privapp-permissions package="android.car.cluster">
+ <permission name="android.permission.CONTROL_INCALL_EXPERIENCE"/>
+ <permission name="android.permission.INTERACT_ACROSS_USERS"/>
+ <permission name="android.permission.MANAGE_USERS"/>
+ <permission name="android.permission.WRITE_SECURE_SETTINGS"/>
+ </privapp-permissions>
+</permissions>
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/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp
index 0a28949..16c8b89 100644
--- a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp
+++ b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp
@@ -161,8 +161,7 @@
// Recording Canvas draw operations: Bitmaps
// ----------------------------------------------------------------------------
-SkiaCanvas::PaintCoW&& SkiaRecordingCanvas::filterBitmap(PaintCoW&& paint,
- sk_sp<SkColorFilter> colorSpaceFilter) {
+SkiaCanvas::PaintCoW&& SkiaRecordingCanvas::filterBitmap(PaintCoW&& paint) {
bool fixBlending = false;
bool fixAA = false;
if (paint) {
@@ -172,23 +171,13 @@
fixAA = paint->isAntiAlias();
}
- if (fixBlending || fixAA || colorSpaceFilter) {
+ if (fixBlending || fixAA) {
SkPaint& tmpPaint = paint.writeable();
if (fixBlending) {
tmpPaint.setBlendMode(SkBlendMode::kDstOut);
}
- if (colorSpaceFilter) {
- if (tmpPaint.getColorFilter()) {
- tmpPaint.setColorFilter(SkColorFilter::MakeComposeFilter(
- tmpPaint.refColorFilter(), std::move(colorSpaceFilter)));
- } else {
- tmpPaint.setColorFilter(std::move(colorSpaceFilter));
- }
- LOG_ALWAYS_FATAL_IF(!tmpPaint.getColorFilter());
- }
-
// disabling AA on bitmap draws matches legacy HWUI behavior
tmpPaint.setAntiAlias(false);
}
@@ -198,7 +187,7 @@
void SkiaRecordingCanvas::drawBitmap(Bitmap& bitmap, float left, float top, const SkPaint* paint) {
sk_sp<SkImage> image = bitmap.makeImage();
- mRecorder.drawImage(image, left, top, filterPaint(paint), bitmap.palette());
+ mRecorder.drawImage(image, left, top, filterBitmap(paint), bitmap.palette());
// if image->unique() is true, then mRecorder.drawImage failed for some reason. It also means
// it is not safe to store a raw SkImage pointer, because the image object will be destroyed
// when this function ends.
@@ -212,7 +201,7 @@
concat(matrix);
sk_sp<SkImage> image = bitmap.makeImage();
- mRecorder.drawImage(image, 0, 0, filterPaint(paint), bitmap.palette());
+ mRecorder.drawImage(image, 0, 0, filterBitmap(paint), bitmap.palette());
if (!bitmap.isImmutable() && image.get() && !image->unique()) {
mDisplayList->mMutableImages.push_back(image.get());
}
@@ -225,7 +214,7 @@
SkRect dstRect = SkRect::MakeLTRB(dstLeft, dstTop, dstRight, dstBottom);
sk_sp<SkImage> image = bitmap.makeImage();
- mRecorder.drawImageRect(image, srcRect, dstRect, filterPaint(paint),
+ mRecorder.drawImageRect(image, srcRect, dstRect, filterBitmap(paint),
SkCanvas::kFast_SrcRectConstraint, bitmap.palette());
if (!bitmap.isImmutable() && image.get() && !image->unique() && !srcRect.isEmpty() &&
!dstRect.isEmpty()) {
@@ -263,7 +252,7 @@
filteredPaint.writeable().setFilterQuality(kLow_SkFilterQuality);
}
sk_sp<SkImage> image = bitmap.makeImage();
- mRecorder.drawImageLattice(image, lattice, dst, filterPaint(std::move(filteredPaint)),
+ mRecorder.drawImageLattice(image, lattice, dst, filterBitmap(std::move(filteredPaint)),
bitmap.palette());
if (!bitmap.isImmutable() && image.get() && !image->unique() && !dst.isEmpty()) {
mDisplayList->mMutableImages.push_back(image.get());
diff --git a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h
index afeccea..c42cea3 100644
--- a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h
+++ b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h
@@ -90,7 +90,7 @@
*/
void initDisplayList(uirenderer::RenderNode* renderNode, int width, int height);
- PaintCoW&& filterBitmap(PaintCoW&& paint, sk_sp<SkColorFilter> colorSpaceFilter);
+ PaintCoW&& filterBitmap(PaintCoW&& paint);
};
} // namespace skiapipeline
diff --git a/media/java/android/media/AudioAttributes.java b/media/java/android/media/AudioAttributes.java
index dc3041f..820d82d 100644
--- a/media/java/android/media/AudioAttributes.java
+++ b/media/java/android/media/AudioAttributes.java
@@ -222,10 +222,10 @@
public final static int SUPPRESSIBLE_MEDIA = 5;
/**
* @hide
- * Denotes a usage for all other sounds not caught in SUPPRESSIBLE_NOTIFICATION,
+ * Denotes a usage for sounds not caught in SUPPRESSIBLE_NOTIFICATION,
* SUPPRESSIBLE_CALL,SUPPRESSIBLE_NEVER, SUPPRESSIBLE_ALARM or SUPPRESSIBLE_MEDIA.
- * This includes system, sonification and unknown sounds.
- * These will be muted when the Zen priority mode doesn't allow sytem sounds
+ * This includes sonification sounds.
+ * These will be muted when the Zen priority mode doesn't allow system sounds
* @see #SUPPRESSIBLE_USAGES
*/
public final static int SUPPRESSIBLE_SYSTEM = 6;
@@ -248,6 +248,7 @@
SUPPRESSIBLE_USAGES.put(USAGE_NOTIFICATION_EVENT, SUPPRESSIBLE_NOTIFICATION);
SUPPRESSIBLE_USAGES.put(USAGE_ASSISTANCE_ACCESSIBILITY, SUPPRESSIBLE_NEVER);
SUPPRESSIBLE_USAGES.put(USAGE_VOICE_COMMUNICATION, SUPPRESSIBLE_NEVER);
+ SUPPRESSIBLE_USAGES.put(USAGE_VOICE_COMMUNICATION_SIGNALLING, SUPPRESSIBLE_NEVER);
SUPPRESSIBLE_USAGES.put(USAGE_ALARM, SUPPRESSIBLE_ALARM);
SUPPRESSIBLE_USAGES.put(USAGE_MEDIA, SUPPRESSIBLE_MEDIA);
SUPPRESSIBLE_USAGES.put(USAGE_ASSISTANCE_NAVIGATION_GUIDANCE, SUPPRESSIBLE_MEDIA);
@@ -255,7 +256,6 @@
SUPPRESSIBLE_USAGES.put(USAGE_ASSISTANT, SUPPRESSIBLE_MEDIA);
/** default volume assignment is STREAM_MUSIC, handle unknown usage as media */
SUPPRESSIBLE_USAGES.put(USAGE_UNKNOWN, SUPPRESSIBLE_MEDIA);
- SUPPRESSIBLE_USAGES.put(USAGE_VOICE_COMMUNICATION_SIGNALLING, SUPPRESSIBLE_SYSTEM);
SUPPRESSIBLE_USAGES.put(USAGE_ASSISTANCE_SONIFICATION, SUPPRESSIBLE_SYSTEM);
}
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/ExifInterface.java b/media/java/android/media/ExifInterface.java
index 58c6be9..5645ba5 100644
--- a/media/java/android/media/ExifInterface.java
+++ b/media/java/android/media/ExifInterface.java
@@ -1794,15 +1794,13 @@
// Set thumbnail image offset and length
setThumbnailData(inputStream);
mIsSupportedFile = true;
- } catch (IOException e) {
+ } catch (IOException | OutOfMemoryError e) {
// Ignore exceptions in order to keep the compatibility with the old versions of
// ExifInterface.
mIsSupportedFile = false;
- if (DEBUG) {
- Log.d(TAG, "Invalid image: ExifInterface got an unsupported image format file"
- + "(ExifInterface supports JPEG and some RAW image formats only) "
- + "or a corrupted JPEG file to ExifInterface.", e);
- }
+ Log.w(TAG, "Invalid image: ExifInterface got an unsupported image format file"
+ + "(ExifInterface supports JPEG and some RAW image formats only) "
+ + "or a corrupted JPEG file to ExifInterface.", e);
} finally {
addDefaultValuesForCompatibility();
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/media/java/android/media/MediaHTTPConnection.java b/media/java/android/media/MediaHTTPConnection.java
index b5c4cca..8ee929e 100644
--- a/media/java/android/media/MediaHTTPConnection.java
+++ b/media/java/android/media/MediaHTTPConnection.java
@@ -38,6 +38,7 @@
import java.net.UnknownServiceException;
import java.util.HashMap;
import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
/** @hide */
public class MediaHTTPConnection extends IMediaHTTPConnection.Stub {
@@ -83,6 +84,10 @@
private final static int HTTP_TEMP_REDIRECT = 307;
private final static int MAX_REDIRECTS = 20;
+ // The number of threads that are currently running disconnect() (possibly
+ // not yet holding the synchronized lock).
+ private final AtomicInteger mNumDisconnectingThreads = new AtomicInteger(0);
+
@UnsupportedAppUsage
public MediaHTTPConnection() {
CookieHandler cookieHandler = CookieHandler.getDefault();
@@ -155,19 +160,24 @@
@Override
@UnsupportedAppUsage
public void disconnect() {
- HttpURLConnection connectionToDisconnect = mConnection;
- // Call disconnect() before blocking for the lock in order to ensure that any
- // other thread that is blocked in readAt() will return quickly.
- if (connectionToDisconnect != null) {
- connectionToDisconnect.disconnect();
- }
- synchronized (this) {
- // It's unlikely but possible that while we were waiting to acquire the lock, another
- // thread concurrently started a new connection; if so, we're disconnecting that one
- // here, too.
- teardownConnection();
- mHeaders = null;
- mURL = null;
+ mNumDisconnectingThreads.incrementAndGet();
+ try {
+ HttpURLConnection connectionToDisconnect = mConnection;
+ // Call disconnect() before blocking for the lock in order to ensure that any
+ // other thread that is blocked in readAt() will return quickly.
+ if (connectionToDisconnect != null) {
+ connectionToDisconnect.disconnect();
+ }
+ synchronized (this) {
+ // It's possible that while we were waiting to acquire the lock, another thread
+ // concurrently started a new connection; if so, we're disconnecting that one
+ // here, too.
+ teardownConnection();
+ mHeaders = null;
+ mURL = null;
+ }
+ } finally {
+ mNumDisconnectingThreads.decrementAndGet();
}
}
@@ -224,11 +234,36 @@
boolean noProxy = isLocalHost(url);
while (true) {
+ // If another thread is concurrently disconnect()ing, there's a race
+ // between them and us. Therefore, we check mNumDisconnectingThreads shortly
+ // (not atomically) before & after writing mConnection. This guarantees that
+ // we won't "lose" a disconnect by creating a new connection that might
+ // miss the disconnect.
+ //
+ // Note that throwing an instanceof IOException is also what this thread
+ // would have done if another thread disconnect()ed the connection while
+ // this thread was blocked reading from that connection further down in this
+ // loop.
+ if (mNumDisconnectingThreads.get() > 0) {
+ throw new IOException("concurrently disconnecting");
+ }
if (noProxy) {
mConnection = (HttpURLConnection)url.openConnection(Proxy.NO_PROXY);
} else {
mConnection = (HttpURLConnection)url.openConnection();
}
+ // If another thread is concurrently disconnecting, throwing IOException will
+ // cause us to release the lock, giving the other thread a chance to acquire
+ // it. It also ensures that the catch block will run, which will tear down
+ // the connection even if the other thread happens to already be on its way
+ // out of disconnect().
+ if (mNumDisconnectingThreads.get() > 0) {
+ throw new IOException("concurrently disconnecting");
+ }
+ // If we get here without having thrown, we know that other threads
+ // will see our write to mConnection. Any disconnect() on that mConnection
+ // instance will cause our read from/write to that connection instance below
+ // to encounter an instanceof IOException.
mConnection.setConnectTimeout(CONNECT_TIMEOUT_MS);
// handle redirects ourselves if we do not allow cross-domain redirect
diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/functional/CameraTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/functional/CameraTest.java
index 7f23ba5..9b643ad 100644
--- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/functional/CameraTest.java
+++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/functional/CameraTest.java
@@ -25,7 +25,6 @@
import android.hardware.Camera.PreviewCallback;
import android.hardware.Camera.ShutterCallback;
import android.os.ConditionVariable;
-import android.os.Environment;
import android.os.Looper;
import android.test.ActivityInstrumentationTestCase;
import android.test.suitebuilder.annotation.LargeTest;
@@ -159,7 +158,7 @@
if (rawData != null) {
int rawDataLength = rawData.length;
File rawoutput = new File(
- Environment.getExternalStorageDirectory().toString(), "/test.bmp");
+ mContext.getExternalFilesDir(null).getPath(), "/test.bmp");
FileOutputStream outstream = new FileOutputStream(rawoutput);
outstream.write(rawData);
Log.v(TAG, "JpegPictureCallback rawDataLength = " + rawDataLength);
diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/helpers/CameraTestHelper.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/helpers/CameraTestHelper.java
index 84153d60..bd236a6 100644
--- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/helpers/CameraTestHelper.java
+++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/helpers/CameraTestHelper.java
@@ -22,10 +22,11 @@
import android.hardware.Camera.Parameters;
import android.hardware.Camera.PictureCallback;
import android.hardware.Camera.ShutterCallback;
-import android.os.Environment;
import android.util.Log;
import android.view.SurfaceHolder;
+import androidx.test.InstrumentationRegistry;
+
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
@@ -70,7 +71,8 @@
try {
Log.v(TAG, "JPEG picture taken");
fos = new FileOutputStream(String.format("%s/%s/%s-%d.jpg",
- Environment.getExternalStorageDirectory(), CAMERA_STRESS_IMAGES_DIRECTORY,
+ InstrumentationRegistry.getInstrumentation().getTargetContext()
+ .getExternalFilesDir(null).getPath(), CAMERA_STRESS_IMAGES_DIRECTORY,
CAMERA_STRESS_IMAGES_PREFIX, System.currentTimeMillis()));
fos.write(data);
} catch (FileNotFoundException e) {
@@ -95,7 +97,8 @@
public void setupCameraTest() {
// Create the test images directory if it doesn't exist
File stressImagesDirectory = new File(String.format("%s/%s",
- Environment.getExternalStorageDirectory(), CAMERA_STRESS_IMAGES_DIRECTORY));
+ InstrumentationRegistry.getInstrumentation().getTargetContext()
+ .getExternalFilesDir(null).getPath(), CAMERA_STRESS_IMAGES_DIRECTORY));
if (!stressImagesDirectory.exists()) {
stressImagesDirectory.mkdir();
}
@@ -129,7 +132,8 @@
public void cleanupTestImages() {
try {
File stressImagesDirectory = new File(String.format("%s/%s",
- Environment.getExternalStorageDirectory(), CAMERA_STRESS_IMAGES_DIRECTORY));
+ InstrumentationRegistry.getInstrumentation().getTargetContext()
+ .getExternalFilesDir(null).getPath(), CAMERA_STRESS_IMAGES_DIRECTORY));
File[] stressImages = stressImagesDirectory.listFiles();
for (File f : stressImages) {
f.delete();
diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/helpers/CameraTestUtils.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/helpers/CameraTestUtils.java
index 0340cec..0ae640d 100644
--- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/helpers/CameraTestUtils.java
+++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/helpers/CameraTestUtils.java
@@ -54,7 +54,6 @@
import android.media.ImageReader;
import android.media.ImageWriter;
import android.os.Build;
-import android.os.Environment;
import android.os.Handler;
import android.util.Log;
import android.util.Pair;
@@ -63,6 +62,8 @@
import android.view.Surface;
import android.view.WindowManager;
+import androidx.test.InstrumentationRegistry;
+
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Array;
@@ -128,7 +129,8 @@
private static final Location sTestLocation2 = new Location(LocationManager.NETWORK_PROVIDER);
protected static final String DEBUG_FILE_NAME_BASE =
- Environment.getExternalStorageDirectory().getPath();
+ InstrumentationRegistry.getInstrumentation().getTargetContext()
+ .getExternalFilesDir(null).getPath();
static {
sTestLocation0.setTime(1199145600L);
diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/stress/Camera2SwitchPreviewTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/stress/Camera2SwitchPreviewTest.java
index 11327ca..a26ee2d 100644
--- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/stress/Camera2SwitchPreviewTest.java
+++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/stress/Camera2SwitchPreviewTest.java
@@ -104,7 +104,6 @@
private static final double AE_COMPENSATION_ERROR_TOLERANCE = 0.2;
// 5 percent error margin for resulting metering regions
private static final float METERING_REGION_ERROR_PERCENT_DELTA = 0.05f;
- private final String VIDEO_FILE_PATH = Environment.getExternalStorageDirectory().getPath();
private static final boolean DEBUG_DUMP = Log.isLoggable(TAG, Log.DEBUG);
private static final int RECORDING_DURATION_MS = 3000;
@@ -137,10 +136,12 @@
private int mVideoFrameRate;
private Size mVideoSize;
private long mRecordingStartTime;
+ private String mVideoFilePath;
@Override
protected void setUp() throws Exception {
super.setUp();
+ mVideoFilePath = mContext.getExternalFilesDir(null).getPath();
}
@Override
@@ -371,9 +372,9 @@
}
// Configure preview and recording surfaces.
- mOutMediaFileName = VIDEO_FILE_PATH + "/test_video.mp4";
+ mOutMediaFileName = mVideoFilePath + "/test_video.mp4";
if (DEBUG_DUMP) {
- mOutMediaFileName = VIDEO_FILE_PATH + "/test_video_" + cameraId + "_"
+ mOutMediaFileName = mVideoFilePath + "/test_video_" + cameraId + "_"
+ videoSz.toString() + ".mp4";
}
diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/stress/CameraStressTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/stress/CameraStressTest.java
index d1193de..74244b9 100644
--- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/stress/CameraStressTest.java
+++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/stress/CameraStressTest.java
@@ -28,7 +28,6 @@
import java.util.List;
import android.hardware.Camera.Parameters;
-import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import android.test.ActivityInstrumentationTestCase2;
@@ -36,6 +35,8 @@
import android.util.Log;
import android.view.SurfaceHolder;
+import androidx.test.InstrumentationRegistry;
+
/**
* Junit / Instrumentation test case for the following camera APIs:
* - camera zoom
@@ -85,7 +86,8 @@
mCameraTestHelper = new CameraTestHelper();
File stressOutFile = new File(String.format("%s/%s",
- Environment.getExternalStorageDirectory(), CAMERA_STRESS_OUTPUT));
+ InstrumentationRegistry.getInstrumentation().getTargetContext()
+ .getExternalFilesDir(null).getPath(), CAMERA_STRESS_OUTPUT));
mOutput = new BufferedWriter(new FileWriter(stressOutFile, true));
mOutput.write(this.getName() + "\n");
}
diff --git a/native/android/surface_control.cpp b/native/android/surface_control.cpp
index 7ee2f0a..8fe4fec 100644
--- a/native/android/surface_control.cpp
+++ b/native/android/surface_control.cpp
@@ -384,6 +384,9 @@
transaction->setCrop(surfaceControl, static_cast<const Rect&>(source));
transaction->setFrame(surfaceControl, static_cast<const Rect&>(destination));
transaction->setTransform(surfaceControl, transform);
+ bool transformToInverseDisplay = (NATIVE_WINDOW_TRANSFORM_INVERSE_DISPLAY & transform) ==
+ NATIVE_WINDOW_TRANSFORM_INVERSE_DISPLAY;
+ transaction->setTransformToDisplayInverse(surfaceControl, transformToInverseDisplay);
}
void ASurfaceTransaction_setBufferTransparency(ASurfaceTransaction* aSurfaceTransaction,
diff --git a/packages/CaptivePortalLogin/src/com/android/captiveportallogin/CaptivePortalLoginActivity.java b/packages/CaptivePortalLogin/src/com/android/captiveportallogin/CaptivePortalLoginActivity.java
index bda5743..23fb7b6 100644
--- a/packages/CaptivePortalLogin/src/com/android/captiveportallogin/CaptivePortalLoginActivity.java
+++ b/packages/CaptivePortalLogin/src/com/android/captiveportallogin/CaptivePortalLoginActivity.java
@@ -485,7 +485,10 @@
if (request.isForMainFrame()) {
mMainFrameUrl = request.getUrl().toString();
}
- return false;
+ // Be careful that two shouldOverrideUrlLoading methods are overridden, but
+ // shouldOverrideUrlLoading(WebView view, String url) was deprecated in API level 24.
+ // TODO: delete deprecated one ??
+ return shouldOverrideUrlLoading(view, mMainFrameUrl);
}
// A web page consisting of a large broken lock icon to indicate SSL failure.
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/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
index da3416b..1b27b52 100644
--- a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
+++ b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
@@ -638,7 +638,7 @@
final String docId = DocumentsContract.getDocumentId(documentUri);
try {
final Bundle out = new Bundle();
- final Uri uri = Uri.fromFile(getFileForDocId(docId));
+ final Uri uri = Uri.fromFile(getFileForDocId(docId, true));
out.putParcelable(DocumentsContract.EXTRA_URI, uri);
return out;
} catch (FileNotFoundException e) {
diff --git a/packages/NetworkStack/src/com/android/server/NetworkStackService.java b/packages/NetworkStack/src/com/android/server/NetworkStackService.java
index 2fae0c7..9bf4457 100644
--- a/packages/NetworkStack/src/com/android/server/NetworkStackService.java
+++ b/packages/NetworkStack/src/com/android/server/NetworkStackService.java
@@ -195,6 +195,7 @@
@Override
public void makeNetworkMonitor(Network network, String name, INetworkMonitorCallbacks cb)
throws RemoteException {
+ checkNetworkStackCallingPermission();
updateSystemAidlVersion(cb.getInterfaceVersion());
final SharedLog log = addValidationLogs(network, name);
final NetworkMonitor nm = new NetworkMonitor(mContext, cb, network, log);
@@ -203,6 +204,7 @@
@Override
public void makeIpClient(String ifName, IIpClientCallbacks cb) throws RemoteException {
+ checkNetworkStackCallingPermission();
updateSystemAidlVersion(cb.getInterfaceVersion());
final IpClient ipClient = new IpClient(mContext, ifName, cb, mObserverRegistry, this);
@@ -228,6 +230,7 @@
@Override
public void fetchIpMemoryStore(@NonNull final IIpMemoryStoreCallbacks cb)
throws RemoteException {
+ checkNetworkStackCallingPermission();
updateSystemAidlVersion(cb.getInterfaceVersion());
cb.onIpMemoryStoreFetched(mIpMemoryStoreService);
}
diff --git a/packages/NetworkStack/src/com/android/server/util/PermissionUtil.java b/packages/NetworkStack/src/com/android/server/util/PermissionUtil.java
index 6fbeead..6701384 100644
--- a/packages/NetworkStack/src/com/android/server/util/PermissionUtil.java
+++ b/packages/NetworkStack/src/com/android/server/util/PermissionUtil.java
@@ -16,30 +16,56 @@
package com.android.server.util;
+import static android.os.Binder.getCallingPid;
import static android.os.Binder.getCallingUid;
import android.os.Process;
import android.os.UserHandle;
+import java.util.concurrent.atomic.AtomicInteger;
+
/**
* Utility class to check calling permissions on the network stack.
*/
public final class PermissionUtil {
+ private static final AtomicInteger sSystemPid = new AtomicInteger(-1);
/**
* Check that the caller is allowed to communicate with the network stack.
* @throws SecurityException The caller is not allowed to communicate with the network stack.
*/
public static void checkNetworkStackCallingPermission() {
- // TODO: check that the calling PID is the system server.
final int caller = getCallingUid();
- if (caller != Process.SYSTEM_UID
- && UserHandle.getAppId(caller) != Process.BLUETOOTH_UID
- && UserHandle.getAppId(caller) != Process.PHONE_UID) {
+ if (caller == Process.SYSTEM_UID) {
+ checkConsistentSystemPid();
+ return;
+ }
+
+ if (UserHandle.getAppId(caller) != Process.BLUETOOTH_UID) {
throw new SecurityException("Invalid caller: " + caller);
}
}
+ private static void checkConsistentSystemPid() {
+ // Apart from the system server process, no process with a system UID should try to
+ // communicate with the network stack. This is to ensure that the network stack does not
+ // need to maintain behavior for clients it was not designed to work with.
+ // Checking that all calls from a system UID originate from the same PID loosely enforces
+ // this restriction as if another system process calls the network stack first, the system
+ // server would lose access to the network stack and cause obvious failures. If the system
+ // server calls the network stack first, other clients would lose access as expected.
+ final int systemPid = getCallingPid();
+ if (sSystemPid.compareAndSet(-1, systemPid)) {
+ // sSystemPid was unset (-1): this was the first call
+ return;
+ }
+
+ if (sSystemPid.get() != systemPid) {
+ throw new SecurityException("Invalid PID for the system server, expected "
+ + sSystemPid.get() + " but was called from " + systemPid);
+ }
+ }
+
/**
* Check that the caller is allowed to dump the network stack, e.g. dumpsys.
* @throws SecurityException The caller is not allowed to dump the network stack.
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/OWNERS b/packages/SystemUI/OWNERS
index c071b8b..83acfa0 100644
--- a/packages/SystemUI/OWNERS
+++ b/packages/SystemUI/OWNERS
@@ -12,6 +12,7 @@
dupin@google.com
ethibodeau@google.com
evanlaird@google.com
+hyunyoungs@google.com
jmonk@google.com
jaggies@google.com
jjaggi@google.com
@@ -24,6 +25,8 @@
lynhan@google.com
madym@google.com
mankoff@google.com
+mrcasey@google.com
+mrenouf@google.com
nbenbernou@google.com
nesciosquid@google.com
ngmatthew@google.com
diff --git a/packages/SystemUI/res/drawable/corner_gesture_hint.xml b/packages/SystemUI/res/drawable/corner_gesture_hint.xml
deleted file mode 100644
index 3f4abb0..0000000
--- a/packages/SystemUI/res/drawable/corner_gesture_hint.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<!--
- Copyright (C) 2019 The Android Open Source Project
-
- 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.
--->
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:height="12dp"
- android:width="12dp"
- android:viewportWidth="12"
- android:viewportHeight="12">
-
- <path android:fillColor="#00000000"
- android:pathData="M 1.18 10.65 C 1.18 5.58 5.41 1.18 10.65 1.18"
- android:strokeColor="#000"
- android:strokeLineCap="round"
- android:strokeWidth="1.3" />
-</vector>
diff --git a/packages/SystemUI/res/drawable/ic_notifications_alert.xml b/packages/SystemUI/res/drawable/ic_notifications_alert.xml
index eb7b8ee..d53d698 100644
--- a/packages/SystemUI/res/drawable/ic_notifications_alert.xml
+++ b/packages/SystemUI/res/drawable/ic_notifications_alert.xml
@@ -14,11 +14,11 @@
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="24.0dp"
- android:height="24.0dp"
- android:viewportWidth="24.0"
- android:viewportHeight="24.0">
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
<path
- android:pathData="M7.58 4.08L6.15 2.65C3.75 4.48 2.17 7.3 2.03 10.5h2c.15-2.65 1.51-4.97 3.55-6.42zm12.39 6.42h2c-.15-3.2-1.73-6.02-4.12-7.85l-1.42 1.43c2.02 1.45 3.39 3.77 3.54 6.42zM18 11c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2v-5zm-6 11c.14 0 .27-.01.4-.04.65-.14 1.18-.58 1.44-1.18.1-.24.15-.5.15-.78h-4c.01 1.1.9 2 2.01 2z"
- android:fillColor="#FF000000"/>
-</vector>
+ android:fillColor="@android:color/white"
+ android:pathData="M18 17v-6c0-3.07-1.63-5.64-4.5-6.32V4c0-0.83-0.67-1.5-1.5-1.5s-1.5 0.67 -1.5 1.5v0.68C7.64 5.36 6 7.92 6 11v6H4v2h16v-2h-2zm-2 0H8v-6c0-2.48 1.51-4.5 4-4.5s4 2.02 4 4.5v6zm-6 3h4c0 1.1-0.9 2-2 2s-2-0.9-2-2zm12-9h-2c0-2.74-1.23-5.19-3.16-6.84l1.41-1.41C20.54 4.77 22 7.71 22 11zM5.75 2.75l1.41 1.41C5.23 5.81 4 8.26 4 11H2c0-3.29 1.46-6.23 3.75-8.25z" />
+</vector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/ic_notifications_silence.xml b/packages/SystemUI/res/drawable/ic_notifications_silence.xml
index ff136eb..a6cc81b 100644
--- a/packages/SystemUI/res/drawable/ic_notifications_silence.xml
+++ b/packages/SystemUI/res/drawable/ic_notifications_silence.xml
@@ -14,15 +14,11 @@
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="24.0dp"
- android:height="24.0dp"
- android:viewportWidth="24.0"
- android:viewportHeight="24.0">
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
<path
- android:pathData="M0 0h24v24H0z"
- />
- <path
- android:pathData="M20 18.69L7.84 6.14 5.27 3.49 4 4.76l2.8 2.8v.01c-.52.99-.8 2.16-.8 3.42v5l-2 2v1h13.73l2 2L21 19.72l-1-1.03zM12 22c1.11 0 2-.89 2-2h-4c0 1.11.89 2 2 2zm6-7.32V11c0-3.08-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68c-.15.03-.29.08-.42.12-.1.03-.2.07-.3.11h-.01c-.01 0-.01 0-.02.01-.23.09-.46.2-.68.31 0 0-.01 0-.01.01L18 14.68z"
- android:fillColor="#FF000000"
- />
-</vector>
+ android:fillColor="@android:color/white"
+ android:pathData="M12 22c1.1 0 2-0.9 2-2h-4c0 1.1 0.9 2 2 2zm4-6L2.81 2.81 1.39 4.22l4.85 4.85C6.09 9.68 6 10.33 6 11v6H4v2h12.17l3.61 3.61 1.41-1.41L16 16zm-8 1l0.01-6.16L14.17 17H8zm4-10.5c2.49 0 4 2.02 4 4.5v2.17l2 2V11c0-3.07-1.63-5.64-4.5-6.32V4c0-0.83-0.67-1.5-1.5-1.5s-1.5 0.67 -1.5 1.5v0.68c-0.78 0.18 -1.45 0.52 -2.04 0.95 L9.93 7.1c0.58-0.37 1.27-0.6 2.07-0.6z" />
+</vector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/ic_volume_media_bt.xml b/packages/SystemUI/res/drawable/ic_volume_media_bt.xml
index 9f7744e..23cb206 100644
--- a/packages/SystemUI/res/drawable/ic_volume_media_bt.xml
+++ b/packages/SystemUI/res/drawable/ic_volume_media_bt.xml
@@ -14,14 +14,12 @@
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="24.0dp"
- android:height="24.0dp"
- android:viewportWidth="24.0"
- android:viewportHeight="24.0"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24"
android:tint="?android:attr/colorControlNormal">
-
<path
- android:fillColor="#FFFFFFFF"
- android:pathData="M9,3l0.01,10.55C8.41,13.21 7.73,13 7.01,13C4.79,13 3,14.79 3,17c0,2.21 1.79,4 4.01,4S11,19.21 11,17V7h4V3H9zM7.01,19c-1.1,0 -2,-0.9 -2,-2c0,-1.1 0.9,-2 2,-2s2,0.9 2,2C9.01,18.1 8.11,19 7.01,19zM21,12.43L17.57,9h-0.6v4.55l-2.75,-2.75l-0.85,0.85L16.73,15l-3.35,3.35l0.85,0.85l2.75,-2.75V21h0.6L21,17.57L18.42,15L21,12.43zM18.17,11.3l1.13,1.13l-1.13,1.13V11.3zM19.3,17.57l-1.13,1.13v-2.26L19.3,17.57z"/>
-
-</vector>
+ android:fillColor="@android:color/white"
+ android:pathData="M9 3l0.01 10.55c-0.6-0.34-1.28-0.55-2-0.55C4.79 13 3 14.79 3 17s1.79 4 4.01 4S11 19.21 11 17V7h4V3H9zm12 9.43L17.57 9h-0.6v4.55l-2.75-2.75-0.85 0.85 L16.73 15l-3.35 3.35 0.85 0.85 2.75-2.75V21h0.6L21 17.57 18.42 15 21 12.43zm-2.83-1.13l1.13 1.13-1.13 1.13V11.3zm1.13 6.27l-1.13 1.13v-2.26l1.13 1.13z" />
+</vector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/ic_volume_media_bt_mute.xml b/packages/SystemUI/res/drawable/ic_volume_media_bt_mute.xml
index 12e0f2e..2469ddc 100644
--- a/packages/SystemUI/res/drawable/ic_volume_media_bt_mute.xml
+++ b/packages/SystemUI/res/drawable/ic_volume_media_bt_mute.xml
@@ -14,14 +14,12 @@
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:height="24dp"
- android:viewportHeight="24.0"
- android:viewportWidth="24.0"
android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24"
android:tint="?android:attr/colorControlNormal">
-
<path
- android:fillColor="#FFFFFFFF"
- android:pathData="M9,6.17L9,3h6v4h-4v1.17L9,6.17zM19.42,15L22,17.57l-0.8,0.8l-6.78,-6.78l0.8,-0.8l2.75,2.75V9h0.6L22,12.43L19.42,15zM19.17,13.55l1.13,-1.13l-1.13,-1.13V13.55zM17.21,17.21l3.98,3.98l-1.41,1.41l-3.98,-3.98l-0.58,0.58l-0.85,-0.85l0.58,-0.58L11,13.83V17c0,2.21 -1.78,4 -3.99,4S3,19.21 3,17c0,-2.21 1.79,-4 4.01,-4c0.73,0 1.41,0.21 2,0.55l0,-1.72L1.39,4.22l1.41,-1.41l13.56,13.56L17.21,17.21zM9.01,17c0,-1.1 -0.9,-2 -2,-2s-2,0.9 -2,2c0,1.1 0.9,2 2,2S9.01,18.1 9.01,17z"/>
-
-</vector>
+ android:fillColor="@android:color/white"
+ android:pathData="M9 6.17V3h6v4h-4v1.17l-2-2zM19.42 15L22 17.57l-0.8 0.8 -6.78-6.78 0.8 -0.8 2.75 2.75V9h0.6L22 12.43 19.42 15zm-0.25-1.45l1.13-1.13-1.13-1.13v2.26zm-1.96 3.66l3.98 3.98-1.41 1.41-3.98-3.98-0.58 0.58 -0.85-0.85 0.58 -0.58L11 13.83V17c0 2.21-1.78 4-3.99 4S3 19.21 3 17s1.79-4 4.01-4c0.73 0 1.41 0.21 2 0.55v-1.72L1.39 4.22 2.8 2.81l13.56 13.56 0.85 0.84z" />
+</vector>
\ No newline at end of file
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/invocation_lights.xml b/packages/SystemUI/res/layout/invocation_lights.xml
new file mode 100644
index 0000000..ff78670
--- /dev/null
+++ b/packages/SystemUI/res/layout/invocation_lights.xml
@@ -0,0 +1,23 @@
+<?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.assist.ui.InvocationLightsView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom"
+ android:visibility="gone"/>
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/layout/rounded_corners.xml b/packages/SystemUI/res/layout/rounded_corners.xml
index 02651a2..b409c8f 100644
--- a/packages/SystemUI/res/layout/rounded_corners.xml
+++ b/packages/SystemUI/res/layout/rounded_corners.xml
@@ -18,36 +18,30 @@
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
+ <com.android.systemui.CornerHandleView
+ android:id="@+id/assist_hint_left"
+ android:layout_width="36dp"
+ android:layout_height="36dp"
+ android:layout_gravity="left|top"
+ android:visibility="gone"/>
+ <com.android.systemui.CornerHandleView
+ android:id="@+id/assist_hint_right"
+ android:layout_width="36dp"
+ android:layout_height="36dp"
+ android:layout_gravity="right|bottom"
+ android:visibility="gone"/>
<ImageView
android:id="@+id/left"
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_gravity="left|top"
android:tint="#ff000000"
- android:src="@drawable/rounded" />
+ android:src="@drawable/rounded"/>
<ImageView
android:id="@+id/right"
android:layout_width="12dp"
android:layout_height="12dp"
android:tint="#ff000000"
android:layout_gravity="right|bottom"
- android:src="@drawable/rounded" />
- <ImageView
- android:id="@+id/assist_hint_left"
- android:layout_width="32dp"
- android:layout_height="32dp"
- android:padding="6dp"
- android:layout_gravity="left|top"
- android:src="@drawable/corner_gesture_hint"
- android:tint="#ffffffff"
- android:visibility="gone" />
- <ImageView
- android:id="@+id/assist_hint_right"
- android:layout_width="32dp"
- android:layout_height="32dp"
- android:padding="6dp"
- android:layout_gravity="right|bottom"
- android:src="@drawable/corner_gesture_hint"
- android:tint="#ffffffff"
- android:visibility="gone" />
+ android:src="@drawable/rounded"/>
</com.android.systemui.RegionInterceptingFrameLayout>
diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml
index 3f84b32..abf4fdf 100644
--- a/packages/SystemUI/res/values/colors.xml
+++ b/packages/SystemUI/res/values/colors.xml
@@ -166,14 +166,17 @@
<color name="smart_reply_button_stroke">#ffdadce0</color>
<!-- Biometric dialog colors -->
- <color name="biometric_dialog_dim_color">#80000000</color> <!-- 50% black -->
+ <color name="biometric_dialog_dim_color">#80000000</color> <!-- 50% black -->
<color name="biometric_dialog_gray">#ff757575</color>
- <color name="biometric_dialog_accent">#ff008577</color> <!-- dark teal -->
- <color name="biometric_dialog_error">#ffd93025</color> <!-- red 600 -->
+ <color name="biometric_dialog_accent">#ff008577</color> <!-- dark teal -->
+ <color name="biometric_dialog_error">#ffd93025</color> <!-- red 600 -->
<!-- Logout button -->
<color name="logout_button_bg_color">#ccffffff</color>
+ <!-- Color for the Assistant invocation lights -->
+ <color name="default_invocation_lights_color">#ffffffff</color> <!-- white -->
+
<!-- GM2 colors -->
<color name="GM2_grey_50">#F8F9FA</color>
<color name="GM2_grey_100">#F1F3F4</color>
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/shared/src/com/android/systemui/shared/system/SystemGestureExclusionListenerCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/SystemGestureExclusionListenerCompat.java
new file mode 100644
index 0000000..9fdecfb
--- /dev/null
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/SystemGestureExclusionListenerCompat.java
@@ -0,0 +1,85 @@
+/**
+ * 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.shared.system;
+
+import android.graphics.Region;
+import android.os.RemoteException;
+import android.util.Log;
+import android.view.ISystemGestureExclusionListener;
+import android.view.WindowManagerGlobal;
+
+/**
+ * Utility class to listen for exclusion rect changes.
+ */
+public abstract class SystemGestureExclusionListenerCompat {
+
+ private static final String TAG = "SGEListenerCompat";
+
+ private final int mDisplayId;
+
+ private ISystemGestureExclusionListener mGestureExclusionListener =
+ new ISystemGestureExclusionListener.Stub() {
+ @Override
+ public void onSystemGestureExclusionChanged(int displayId,
+ Region systemGestureExclusion) {
+ if (displayId == mDisplayId) {
+ onExclusionChanged(systemGestureExclusion);
+ }
+ }
+ };
+ private boolean mRegistered;
+
+ public SystemGestureExclusionListenerCompat(int displayId) {
+ mDisplayId = displayId;
+ }
+
+ /**
+ * Called when the exclusion region has changed
+ */
+ public abstract void onExclusionChanged(Region systemGestureExclusion);
+
+ /**
+ * Registers the listener for getting exclusion rect changes.
+ */
+ public void register() {
+ if (!mRegistered) {
+ try {
+ WindowManagerGlobal.getWindowManagerService()
+ .registerSystemGestureExclusionListener(
+ mGestureExclusionListener, mDisplayId);
+ mRegistered = true;
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to register window manager callbacks", e);
+ }
+ }
+ }
+
+ /**
+ * Unregisters the receiver if previously registered
+ */
+ public void unregister() {
+ if (mRegistered) {
+ try {
+ WindowManagerGlobal.getWindowManagerService()
+ .unregisterSystemGestureExclusionListener(
+ mGestureExclusionListener, mDisplayId);
+ mRegistered = false;
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to unregister window manager callbacks", e);
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/BatteryMeterView.java b/packages/SystemUI/src/com/android/systemui/BatteryMeterView.java
index de4c798..bce5c23 100644
--- a/packages/SystemUI/src/com/android/systemui/BatteryMeterView.java
+++ b/packages/SystemUI/src/com/android/systemui/BatteryMeterView.java
@@ -322,9 +322,6 @@
mCharging = pluggedIn;
mLevel = level;
updatePercentText();
- setContentDescription(
- getContext().getString(charging ? R.string.accessibility_battery_level_charging
- : R.string.accessibility_battery_level, level));
}
@Override
@@ -358,6 +355,9 @@
mBatteryController.getEstimatedTimeRemainingString((String estimate) -> {
if (estimate != null) {
mBatteryPercentView.setText(estimate);
+ setContentDescription(getContext().getString(
+ R.string.battery_low_percent_format_hybrid, mLevel, estimate));
+
} else {
setPercentTextAtCurrentLevel();
}
@@ -371,6 +371,9 @@
private void setPercentTextAtCurrentLevel() {
mBatteryPercentView.setText(
NumberFormat.getPercentInstance().format(mLevel / 100f));
+ setContentDescription(
+ getContext().getString(mCharging ? R.string.accessibility_battery_level_charging
+ : R.string.accessibility_battery_level, mLevel));
}
private void updateShowPercent() {
diff --git a/packages/SystemUI/src/com/android/systemui/CornerHandleView.java b/packages/SystemUI/src/com/android/systemui/CornerHandleView.java
new file mode 100644
index 0000000..528db5a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/CornerHandleView.java
@@ -0,0 +1,143 @@
+/*
+ * 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;
+
+import android.animation.ArgbEvaluator;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.os.SystemProperties;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.view.ContextThemeWrapper;
+import android.view.View;
+
+import com.android.settingslib.Utils;
+
+/**
+ * CornerHandleView draws an inset arc intended to be displayed within the screen decoration
+ * corners.
+ */
+public class CornerHandleView extends View {
+ private static final boolean ALLOW_TUNING = false;
+ private static final int ANGLE_DEGREES = 50;
+ public static final int MARGIN_DP = 11;
+ public static final int RADIUS_DP = 37;
+ public static final float STROKE_DP = 2.5f;
+
+ private Paint mPaint;
+ private int mLightColor;
+ private int mDarkColor;
+ private RectF mOval;
+
+
+ public CornerHandleView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ mPaint = new Paint();
+ mPaint.setAntiAlias(true);
+ mPaint.setStyle(Paint.Style.STROKE);
+ mPaint.setStrokeCap(Paint.Cap.ROUND);
+ mPaint.setStrokeWidth(getStrokePx());
+
+ final int dualToneDarkTheme = Utils.getThemeAttr(mContext,
+ R.attr.darkIconTheme);
+ final int dualToneLightTheme = Utils.getThemeAttr(mContext,
+ R.attr.lightIconTheme);
+ Context lightContext = new ContextThemeWrapper(mContext, dualToneLightTheme);
+ Context darkContext = new ContextThemeWrapper(mContext, dualToneDarkTheme);
+ mLightColor = Utils.getColorAttrDefaultColor(lightContext,
+ R.attr.singleToneColor);
+ mDarkColor = Utils.getColorAttrDefaultColor(darkContext,
+ R.attr.singleToneColor);
+
+ updateOval();
+ }
+
+ /**
+ * Receives an intensity from 0 (lightest) to 1 (darkest) and sets the handle color
+ * approriately. Intention is to match the home handle color.
+ */
+ public void updateDarkness(float darkIntensity) {
+ mPaint.setColor((int) ArgbEvaluator.getInstance().evaluate(darkIntensity,
+ mLightColor,
+ mDarkColor));
+ invalidate();
+ }
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ if (ALLOW_TUNING) {
+ mPaint.setStrokeWidth(getStrokePx());
+ updateOval();
+ }
+
+ canvas.drawArc(mOval, 180 + ((90 - getAngle()) / 2), getAngle(), false,
+ mPaint);
+ }
+
+ // TODO(b/133834204): Remove tweaking of corner handles
+ private static float convertDpToPixel(float dp, Context context) {
+ return dp * ((float) context.getResources().getDisplayMetrics().densityDpi
+ / DisplayMetrics.DENSITY_DEFAULT);
+ }
+
+ private void updateOval() {
+ mOval = new RectF(getMarginPx() - (getStrokePx() / 2.f),
+ getMarginPx() - (getStrokePx() / 2.f),
+ getMarginPx() + (2 * (getRadiusPx()) + (getStrokePx() / 2.f)),
+ getMarginPx() + 2 * getRadiusPx() + (getStrokePx() / 2.f));
+ }
+
+ private int getAngle() {
+ if (ALLOW_TUNING) {
+ return SystemProperties.getInt("CORNER_HANDLE_ANGLE_DEGREES", ANGLE_DEGREES);
+ } else {
+ return ANGLE_DEGREES;
+ }
+ }
+
+ private int getMarginPx() {
+ if (ALLOW_TUNING) {
+ return SystemProperties.getInt("CORNER_HANDLE_MARGIN_PX",
+ (int) convertDpToPixel(MARGIN_DP, getContext()));
+ } else {
+ return (int) convertDpToPixel(MARGIN_DP, getContext());
+ }
+ }
+
+ private int getRadiusPx() {
+ if (ALLOW_TUNING) {
+ return SystemProperties.getInt("CORNER_HANDLE_RADIUS_PX",
+ (int) convertDpToPixel(RADIUS_DP, getContext()));
+ } else {
+ return (int) convertDpToPixel(RADIUS_DP, getContext());
+ }
+ }
+
+ private int getStrokePx() {
+ if (ALLOW_TUNING) {
+ return SystemProperties.getInt("CORNER_HANDLE_STROKE_PX",
+ (int) convertDpToPixel(STROKE_DP, getContext()));
+ } else {
+ return (int) convertDpToPixel(STROKE_DP, getContext());
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
index 390a9e6..4aaf85a 100644
--- a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
+++ b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
@@ -60,6 +60,12 @@
import android.view.ViewGroup.LayoutParams;
import android.view.ViewTreeObserver;
import android.view.WindowManager;
+import android.view.animation.Animation;
+import android.view.animation.AnimationSet;
+import android.view.animation.OvershootInterpolator;
+import android.view.animation.PathInterpolator;
+import android.view.animation.ScaleAnimation;
+import android.view.animation.TranslateAnimation;
import android.widget.FrameLayout;
import android.widget.ImageView;
@@ -72,6 +78,7 @@
import com.android.systemui.plugins.qs.QS;
import com.android.systemui.qs.SecureSetting;
import com.android.systemui.statusbar.phone.CollapsedStatusBarFragment;
+import com.android.systemui.statusbar.phone.NavigationBarTransitions;
import com.android.systemui.statusbar.phone.StatusBar;
import com.android.systemui.tuner.TunablePadding;
import com.android.systemui.tuner.TunerService;
@@ -85,7 +92,8 @@
* An overlay that draws screen decorations in software (e.g for rounded corners or display cutout)
* for antialiasing and emulation purposes.
*/
-public class ScreenDecorations extends SystemUI implements Tunable {
+public class ScreenDecorations extends SystemUI implements Tunable,
+ NavigationBarTransitions.DarkIntensityListener {
private static final boolean DEBUG = false;
private static final String TAG = "ScreenDecorations";
@@ -93,13 +101,17 @@
public static final String PADDING = "sysui_rounded_content_padding";
private static final boolean DEBUG_SCREENSHOT_ROUNDED_CORNERS =
SystemProperties.getBoolean("debug.screenshot_rounded_corners", false);
+ private static final boolean VERBOSE = false;
private DisplayManager mDisplayManager;
private DisplayManager.DisplayListener mDisplayListener;
- @VisibleForTesting protected int mRoundedDefault;
- @VisibleForTesting protected int mRoundedDefaultTop;
- @VisibleForTesting protected int mRoundedDefaultBottom;
+ @VisibleForTesting
+ protected int mRoundedDefault;
+ @VisibleForTesting
+ protected int mRoundedDefaultTop;
+ @VisibleForTesting
+ protected int mRoundedDefaultBottom;
private View mOverlay;
private View mBottomOverlay;
private float mDensity;
@@ -111,6 +123,8 @@
private SecureSetting mColorInversionSetting;
private boolean mPendingRotationChange;
private Handler mHandler;
+ private boolean mAssistHintBlocked = false;
+ private boolean mIsReceivingNavBarColor = false;
/**
* Converts a set of {@link Rect}s into a {@link Region}
@@ -137,15 +151,32 @@
putComponent(ScreenDecorations.class, this);
}
- private void fade(View view, boolean fadeIn) {
+ private void fade(View view, boolean fadeIn, boolean isLeft) {
if (fadeIn) {
view.animate().cancel();
- view.setAlpha(0f);
+ view.setAlpha(1f);
view.setVisibility(View.VISIBLE);
- view.animate().alpha(1f);
+
+ AnimationSet anim = new AnimationSet(true);
+ anim.setDuration(900);
+
+ Animation scaleAnimation = new ScaleAnimation(2f, 1f, 2f, 1f,
+ ScaleAnimation.RELATIVE_TO_SELF, 0.5f, ScaleAnimation.RELATIVE_TO_SELF, 0.5f);
+ anim.addAnimation(scaleAnimation);
+ anim.setInterpolator(new PathInterpolator(0.02f, 0.44f, 0.67f, 1.00f));
+
+ Animation translateAnimation = new TranslateAnimation(
+ TranslateAnimation.RELATIVE_TO_SELF, isLeft ? -0.2f : 0.2f,
+ TranslateAnimation.RELATIVE_TO_SELF,
+ 0f,
+ TranslateAnimation.RELATIVE_TO_SELF, 0.2f, TranslateAnimation.RELATIVE_TO_SELF,
+ 0f);
+ anim.addAnimation(translateAnimation);
+ anim.setInterpolator(new OvershootInterpolator());
+ view.startAnimation(anim);
} else {
view.animate().cancel();
- view.animate().alpha(0f).withEndAction(() -> view.setVisibility(View.INVISIBLE));
+ view.animate().setDuration(400).alpha(0f);
}
}
@@ -161,35 +192,59 @@
return;
}
+ if (mAssistHintBlocked && visible) {
+ if (VERBOSE) {
+ Log.v(TAG, "Assist hint blocked, cannot make it visible");
+ }
+ return;
+ }
+
if (mAssistHintVisible != visible) {
mAssistHintVisible = visible;
- View assistHintTopLeft = mOverlay.findViewById(R.id.assist_hint_left);
- View assistHintTopRight = mOverlay.findViewById(R.id.assist_hint_right);
- View assistHintBottomLeft = mBottomOverlay.findViewById(R.id.assist_hint_left);
- View assistHintBottomRight = mBottomOverlay.findViewById(R.id.assist_hint_right);
+ CornerHandleView assistHintTopLeft = mOverlay.findViewById(R.id.assist_hint_left);
+ CornerHandleView assistHintTopRight = mOverlay.findViewById(R.id.assist_hint_right);
+ CornerHandleView assistHintBottomLeft = mBottomOverlay.findViewById(
+ R.id.assist_hint_left);
+ CornerHandleView assistHintBottomRight = mBottomOverlay.findViewById(
+ R.id.assist_hint_right);
switch (mRotation) {
case RotationUtils.ROTATION_NONE:
- fade(assistHintBottomLeft, mAssistHintVisible);
- fade(assistHintBottomRight, mAssistHintVisible);
+ fade(assistHintBottomLeft, mAssistHintVisible, /* isLeft = */ true);
+ fade(assistHintBottomRight, mAssistHintVisible, /* isLeft = */ false);
break;
case RotationUtils.ROTATION_LANDSCAPE:
- fade(assistHintTopRight, mAssistHintVisible);
- fade(assistHintBottomRight, mAssistHintVisible);
+ fade(assistHintTopRight, mAssistHintVisible, /* isLeft = */ true);
+ fade(assistHintBottomRight, mAssistHintVisible, /* isLeft = */ false);
break;
case RotationUtils.ROTATION_SEASCAPE:
- fade(assistHintTopLeft, mAssistHintVisible);
- fade(assistHintBottomLeft, mAssistHintVisible);
+ fade(assistHintTopLeft, mAssistHintVisible, /* isLeft = */ false);
+ fade(assistHintBottomLeft, mAssistHintVisible, /* isLeft = */ true);
break;
case RotationUtils.ROTATION_UPSIDE_DOWN:
- fade(assistHintTopLeft, mAssistHintVisible);
- fade(assistHintTopRight, mAssistHintVisible);
+ fade(assistHintTopLeft, mAssistHintVisible, /* isLeft = */ false);
+ fade(assistHintTopRight, mAssistHintVisible, /* isLeft = */ true);
break;
}
}
}
+ /**
+ * Prevents the assist hint from becoming visible even if `mAssistHintVisible` is true.
+ */
+ public void setAssistHintBlocked(boolean blocked) {
+ if (!mHandler.getLooper().isCurrentThread()) {
+ mHandler.post(() -> setAssistHintBlocked(blocked));
+ return;
+ }
+
+ mAssistHintBlocked = blocked;
+ if (mAssistHintVisible && mAssistHintBlocked) {
+ setAssistHintVisible(false);
+ }
+ }
+
@VisibleForTesting
Handler startHandlerThread() {
HandlerThread thread = new HandlerThread("ScreenDecorations");
@@ -253,12 +308,12 @@
.inflate(R.layout.rounded_corners, null);
mCutoutTop = new DisplayCutoutView(mContext, true,
this::updateWindowVisibilities, this);
- ((ViewGroup)mOverlay).addView(mCutoutTop);
+ ((ViewGroup) mOverlay).addView(mCutoutTop);
mBottomOverlay = LayoutInflater.from(mContext)
.inflate(R.layout.rounded_corners, null);
mCutoutBottom = new DisplayCutoutView(mContext, false,
this::updateWindowVisibilities, this);
- ((ViewGroup)mBottomOverlay).addView(mCutoutBottom);
+ ((ViewGroup) mBottomOverlay).addView(mCutoutBottom);
mOverlay.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
mOverlay.setAlpha(0);
@@ -378,10 +433,26 @@
if (mOverlay != null) {
updateLayoutParams();
updateViews();
+ if (mAssistHintVisible) {
+ // If assist handles are visible, hide them without animation and then make them
+ // show once again (with corrected rotation).
+ hideAssistHandles();
+ setAssistHintVisible(true);
+ }
}
}
}
+ private void hideAssistHandles() {
+ if (mOverlay != null && mBottomOverlay != null) {
+ mOverlay.findViewById(R.id.assist_hint_left).setVisibility(View.GONE);
+ mOverlay.findViewById(R.id.assist_hint_right).setVisibility(View.GONE);
+ mBottomOverlay.findViewById(R.id.assist_hint_left).setVisibility(View.GONE);
+ mBottomOverlay.findViewById(R.id.assist_hint_right).setVisibility(View.GONE);
+ mAssistHintVisible = false;
+ }
+ }
+
private void updateRoundedCornerRadii() {
final int newRoundedDefault = mContext.getResources().getDimensionPixelSize(
com.android.internal.R.dimen.rounded_corner_radius);
@@ -416,7 +487,7 @@
} else if (mRotation == RotationUtils.ROTATION_LANDSCAPE) {
updateView(topLeft, Gravity.TOP | Gravity.LEFT, 0);
updateView(topRight, Gravity.BOTTOM | Gravity.LEFT, 270);
- updateView(bottomLeft, Gravity.TOP | Gravity.RIGHT, 90);;
+ updateView(bottomLeft, Gravity.TOP | Gravity.RIGHT, 90);
updateView(bottomRight, Gravity.BOTTOM | Gravity.RIGHT, 180);
} else if (mRotation == RotationUtils.ROTATION_UPSIDE_DOWN) {
updateView(topLeft, Gravity.BOTTOM | Gravity.LEFT, 270);
@@ -477,7 +548,7 @@
}
private void updateView(View v, int gravity, int rotation) {
- ((FrameLayout.LayoutParams)v.getLayoutParams()).gravity = gravity;
+ ((FrameLayout.LayoutParams) v.getLayoutParams()).gravity = gravity;
v.setRotation(rotation);
}
@@ -613,6 +684,10 @@
setSize(mOverlay.findViewById(R.id.right), sizeTop);
setSize(mBottomOverlay.findViewById(R.id.left), sizeBottom);
setSize(mBottomOverlay.findViewById(R.id.right), sizeBottom);
+ setSize(mOverlay.findViewById(R.id.assist_hint_left), sizeTop * 2);
+ setSize(mOverlay.findViewById(R.id.assist_hint_right), sizeTop * 2);
+ setSize(mBottomOverlay.findViewById(R.id.assist_hint_left), sizeBottom * 2);
+ setSize(mBottomOverlay.findViewById(R.id.assist_hint_right), sizeBottom * 2);
}
});
}
@@ -624,6 +699,27 @@
view.setLayoutParams(params);
}
+ @Override
+ public void onDarkIntensity(float darkIntensity) {
+ if (mOverlay != null) {
+ CornerHandleView assistHintTopLeft = mOverlay.findViewById(R.id.assist_hint_left);
+ CornerHandleView assistHintTopRight = mOverlay.findViewById(R.id.assist_hint_right);
+
+ assistHintTopLeft.updateDarkness(darkIntensity);
+ assistHintTopRight.updateDarkness(darkIntensity);
+ }
+
+ if (mBottomOverlay != null) {
+ CornerHandleView assistHintBottomLeft = mBottomOverlay.findViewById(
+ R.id.assist_hint_left);
+ CornerHandleView assistHintBottomRight = mBottomOverlay.findViewById(
+ R.id.assist_hint_right);
+
+ assistHintBottomLeft.updateDarkness(darkIntensity);
+ assistHintBottomRight.updateDarkness(darkIntensity);
+ }
+ }
+
@VisibleForTesting
static class TunablePaddingTagListener implements FragmentListener {
diff --git a/packages/SystemUI/src/com/android/systemui/assist/AssistHandleBehavior.java b/packages/SystemUI/src/com/android/systemui/assist/AssistHandleBehavior.java
index e2c7313..2eda3d7 100644
--- a/packages/SystemUI/src/com/android/systemui/assist/AssistHandleBehavior.java
+++ b/packages/SystemUI/src/com/android/systemui/assist/AssistHandleBehavior.java
@@ -16,31 +16,10 @@
package com.android.systemui.assist;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.systemui.assist.AssistHandleBehaviorController.BehaviorController;
-
public enum AssistHandleBehavior {
- TEST(new AssistHandleOffBehavior()),
- OFF(new AssistHandleOffBehavior()),
- LIKE_HOME(new AssistHandleLikeHomeBehavior()),
- REMINDER_EXP(new AssistHandleReminderExpBehavior());
-
- private BehaviorController mController;
-
- AssistHandleBehavior(BehaviorController controller) {
- mController = controller;
- }
-
- BehaviorController getController() {
- return mController;
- }
-
- @VisibleForTesting
- void setTestController(BehaviorController controller) {
- if (this.equals(TEST)) {
- mController = controller;
- }
- }
+ TEST,
+ OFF,
+ LIKE_HOME,
+ REMINDER_EXP;
}
diff --git a/packages/SystemUI/src/com/android/systemui/assist/AssistHandleBehaviorController.java b/packages/SystemUI/src/com/android/systemui/assist/AssistHandleBehaviorController.java
index 0203d9e..01deb03 100644
--- a/packages/SystemUI/src/com/android/systemui/assist/AssistHandleBehaviorController.java
+++ b/packages/SystemUI/src/com/android/systemui/assist/AssistHandleBehaviorController.java
@@ -16,24 +16,27 @@
package com.android.systemui.assist;
-import android.content.BroadcastReceiver;
+import android.content.ComponentName;
import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.os.Build;
import android.os.Handler;
import android.os.SystemClock;
-import android.os.SystemProperties;
+import android.provider.DeviceConfig;
import android.util.Log;
+import androidx.annotation.Nullable;
+
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.app.AssistUtils;
+import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
+import com.android.keyguard.KeyguardUpdateMonitor;
import com.android.systemui.Dependency;
import com.android.systemui.ScreenDecorations;
import com.android.systemui.SysUiServiceProvider;
import com.android.systemui.shared.system.QuickStepContract;
import com.android.systemui.statusbar.phone.NavigationModeController;
-import java.util.Locale;
+import java.util.EnumMap;
+import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
@@ -46,60 +49,76 @@
public final class AssistHandleBehaviorController implements AssistHandleCallbacks {
private static final String TAG = "AssistHandleBehavior";
- private static final boolean IS_DEBUG_DEVICE =
- Build.TYPE.toLowerCase(Locale.ROOT).contains("debug")
- || Build.TYPE.toLowerCase(Locale.ROOT).equals("eng");
- private static final String SHOWN_FREQUENCY_THRESHOLD_KEY =
- "ASSIST_HANDLES_SHOWN_FREQUENCY_THRESHOLD_MS";
private static final long DEFAULT_SHOWN_FREQUENCY_THRESHOLD_MS = TimeUnit.SECONDS.toMillis(10);
- private static final String SHOW_AND_GO_DURATION_KEY = "ASSIST_HANDLES_SHOW_AND_GO_DURATION_MS";
private static final long DEFAULT_SHOW_AND_GO_DURATION_MS = TimeUnit.SECONDS.toMillis(3);
- private static final String BEHAVIOR_KEY = "behavior";
- private static final String SET_BEHAVIOR_ACTION =
- "com.android.systemui.SET_ASSIST_HANDLE_BEHAVIOR";
+
+ /**
+ * This is the default behavior that will be used once the system is up. It will be set once the
+ * behavior dependencies are available. This ensures proper behavior lifecycle.
+ */
+ private static final AssistHandleBehavior DEFAULT_BEHAVIOR = AssistHandleBehavior.REMINDER_EXP;
private final Context mContext;
+ private final AssistUtils mAssistUtils;
private final Handler mHandler;
private final Runnable mHideHandles = this::hideHandles;
private final Supplier<ScreenDecorations> mScreenDecorationsSupplier;
+ private final Map<AssistHandleBehavior, BehaviorController> mBehaviorMap =
+ new EnumMap<>(AssistHandleBehavior.class);
private boolean mHandlesShowing = false;
private long mHandlesLastHiddenAt;
+ /**
+ * This should always be initialized as {@link AssistHandleBehavior#OFF} to ensure proper
+ * behavior lifecycle.
+ */
private AssistHandleBehavior mCurrentBehavior = AssistHandleBehavior.OFF;
private boolean mInGesturalMode;
- AssistHandleBehaviorController(Context context, Handler handler) {
- this(context, handler, () ->
- SysUiServiceProvider.getComponent(context, ScreenDecorations.class));
+ AssistHandleBehaviorController(Context context, AssistUtils assistUtils, Handler handler) {
+ this(
+ context,
+ assistUtils,
+ handler, () -> SysUiServiceProvider.getComponent(context, ScreenDecorations.class),
+ /* testBehavior = */ null);
}
@VisibleForTesting
AssistHandleBehaviorController(
Context context,
+ AssistUtils assistUtils,
Handler handler,
- Supplier<ScreenDecorations> screenDecorationsSupplier) {
+ Supplier<ScreenDecorations> screenDecorationsSupplier,
+ @Nullable BehaviorController testBehavior) {
mContext = context;
+ mAssistUtils = assistUtils;
mHandler = handler;
mScreenDecorationsSupplier = screenDecorationsSupplier;
+ mBehaviorMap.put(AssistHandleBehavior.OFF, new AssistHandleOffBehavior());
+ mBehaviorMap.put(AssistHandleBehavior.LIKE_HOME, new AssistHandleLikeHomeBehavior());
+ mBehaviorMap.put(AssistHandleBehavior.REMINDER_EXP, new AssistHandleReminderExpBehavior());
+ if (testBehavior != null) {
+ mBehaviorMap.put(AssistHandleBehavior.TEST, testBehavior);
+ }
+
mInGesturalMode = QuickStepContract.isGesturalMode(
Dependency.get(NavigationModeController.class)
.addListener(this::handleNavigationModeChange));
- if (IS_DEBUG_DEVICE) {
- context.registerReceiver(new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- String behaviorString = intent.getExtras().getString(BEHAVIOR_KEY);
- try {
- setBehavior(AssistHandleBehavior.valueOf(behaviorString));
- } catch (IllegalArgumentException e) {
- Log.e(TAG, "Invalid behavior identifier: " + behaviorString);
+ setBehavior(DeviceConfig.getString(
+ DeviceConfig.NAMESPACE_SYSTEMUI,
+ SystemUiDeviceConfigFlags.ASSIST_HANDLES_BEHAVIOR_MODE,
+ DEFAULT_BEHAVIOR.toString()));
+ DeviceConfig.addOnPropertyChangedListener(
+ DeviceConfig.NAMESPACE_SYSTEMUI,
+ mHandler::post,
+ (namespace, name, value) -> {
+ if (SystemUiDeviceConfigFlags.ASSIST_HANDLES_BEHAVIOR_MODE.equals(name)) {
+ setBehavior(value);
}
- }
- }, new IntentFilter(SET_BEHAVIOR_ACTION));
- }
+ });
}
@Override
@@ -123,26 +142,56 @@
mHandler.post(() -> maybeShowHandles(/* ignoreThreshold = */ true));
}
+ void onAssistantGesturePerformed() {
+ mBehaviorMap.get(mCurrentBehavior).onAssistantGesturePerformed();
+ }
+
void setBehavior(AssistHandleBehavior behavior) {
if (mCurrentBehavior == behavior) {
return;
}
+ if (!mBehaviorMap.containsKey(behavior)) {
+ Log.e(TAG, "Unsupported behavior requested: " + behavior.toString());
+ return;
+ }
+
if (mInGesturalMode) {
- mCurrentBehavior.getController().onModeDeactivated();
- behavior.getController().onModeActivated(mContext, this);
+ mBehaviorMap.get(mCurrentBehavior).onModeDeactivated();
+ mBehaviorMap.get(behavior).onModeActivated(mContext, /* callbacks = */ this);
}
mCurrentBehavior = behavior;
}
- private static long getShownFrequencyThreshold() {
- return SystemProperties.getLong(
- SHOWN_FREQUENCY_THRESHOLD_KEY, DEFAULT_SHOWN_FREQUENCY_THRESHOLD_MS);
+ private void setBehavior(@Nullable String behavior) {
+ try {
+ setBehavior(AssistHandleBehavior.valueOf(behavior));
+ } catch (IllegalArgumentException | NullPointerException e) {
+ Log.e(TAG, "Invalid behavior: " + behavior, e);
+ }
}
- private static long getShowAndGoDuration() {
- return SystemProperties.getLong(SHOW_AND_GO_DURATION_KEY, DEFAULT_SHOW_AND_GO_DURATION_MS);
+ private boolean handlesUnblocked(boolean ignoreThreshold) {
+ long timeSinceHidden = SystemClock.elapsedRealtime() - mHandlesLastHiddenAt;
+ boolean notThrottled = ignoreThreshold || timeSinceHidden > getShownFrequencyThreshold();
+ ComponentName assistantComponent =
+ mAssistUtils.getAssistComponentForUser(KeyguardUpdateMonitor.getCurrentUser());
+ return notThrottled && assistantComponent != null;
+ }
+
+ private long getShownFrequencyThreshold() {
+ return DeviceConfig.getLong(
+ DeviceConfig.NAMESPACE_SYSTEMUI,
+ SystemUiDeviceConfigFlags.ASSIST_HANDLES_SHOWN_FREQUENCY_THRESHOLD_MS,
+ DEFAULT_SHOWN_FREQUENCY_THRESHOLD_MS);
+ }
+
+ private long getShowAndGoDuration() {
+ return DeviceConfig.getLong(
+ DeviceConfig.NAMESPACE_SYSTEMUI,
+ SystemUiDeviceConfigFlags.ASSIST_HANDLES_SHOW_AND_GO_DURATION_MS,
+ DEFAULT_SHOW_AND_GO_DURATION_MS);
}
private void maybeShowHandles(boolean ignoreThreshold) {
@@ -150,8 +199,7 @@
return;
}
- long timeSinceHidden = SystemClock.elapsedRealtime() - mHandlesLastHiddenAt;
- if (ignoreThreshold || timeSinceHidden > getShownFrequencyThreshold()) {
+ if (handlesUnblocked(ignoreThreshold)) {
mHandlesShowing = true;
ScreenDecorations screenDecorations = mScreenDecorationsSupplier.get();
if (screenDecorations == null) {
@@ -185,9 +233,9 @@
mInGesturalMode = inGesturalMode;
if (mInGesturalMode) {
- mCurrentBehavior.getController().onModeActivated(mContext, this);
+ mBehaviorMap.get(mCurrentBehavior).onModeActivated(mContext, /* callbacks = */ this);
} else {
- mCurrentBehavior.getController().onModeDeactivated();
+ mBehaviorMap.get(mCurrentBehavior).onModeDeactivated();
hide();
}
}
@@ -199,6 +247,7 @@
interface BehaviorController {
void onModeActivated(Context context, AssistHandleCallbacks callbacks);
- void onModeDeactivated();
+ default void onModeDeactivated() {}
+ default void onAssistantGesturePerformed() {}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/assist/AssistHandleLikeHomeBehavior.java b/packages/SystemUI/src/com/android/systemui/assist/AssistHandleLikeHomeBehavior.java
index e89e93c..05e504c 100644
--- a/packages/SystemUI/src/com/android/systemui/assist/AssistHandleLikeHomeBehavior.java
+++ b/packages/SystemUI/src/com/android/systemui/assist/AssistHandleLikeHomeBehavior.java
@@ -16,17 +16,15 @@
package com.android.systemui.assist;
-import android.app.StatusBarManager;
import android.content.Context;
import androidx.annotation.Nullable;
import com.android.systemui.Dependency;
-import com.android.systemui.SysUiServiceProvider;
import com.android.systemui.assist.AssistHandleBehaviorController.BehaviorController;
-import com.android.systemui.statusbar.CommandQueue;
-import com.android.systemui.statusbar.NavigationBarController;
-import com.android.systemui.statusbar.phone.NavigationBarFragment;
+import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.recents.OverviewProxyService;
+import com.android.systemui.shared.system.QuickStepContract;
/**
* Assistant Handle behavior that makes Assistant handles show/hide when the home handle is
@@ -34,47 +32,68 @@
*/
final class AssistHandleLikeHomeBehavior implements BehaviorController {
- private final CommandQueue.Callbacks mCallbacks = new CommandQueue.Callbacks() {
+ private final StatusBarStateController.StateListener mStatusBarStateListener =
+ new StatusBarStateController.StateListener() {
+ @Override
+ public void onDozingChanged(boolean isDozing) {
+ handleDozingChanged(isDozing);
+ }
+ };
+ private final OverviewProxyService.OverviewProxyListener mOverviewProxyListener =
+ new OverviewProxyService.OverviewProxyListener() {
@Override
- public void setWindowState(int displayId, int window, int state) {
- if (mNavBarDisplayId == displayId
- && window == StatusBarManager.WINDOW_NAVIGATION_BAR) {
- handleWindowStateChanged(state);
- }
+ public void onSystemUiStateChanged(int sysuiStateFlags) {
+ handleSystemUiStateChange(sysuiStateFlags);
}
};
+ private final StatusBarStateController mStatusBarStateController;
+ private final OverviewProxyService mOverviewProxyService;
- private CommandQueue mCommandQueue;
- private int mNavBarDisplayId;
- private boolean mIsNavBarWindowVisible;
+ private boolean mIsDozing;
+ private boolean mIsHomeHandleHiding;
@Nullable private AssistHandleCallbacks mAssistHandleCallbacks;
+ AssistHandleLikeHomeBehavior() {
+ mStatusBarStateController = Dependency.get(StatusBarStateController.class);
+ mOverviewProxyService = Dependency.get(OverviewProxyService.class);
+ }
+
@Override
public void onModeActivated(Context context, AssistHandleCallbacks callbacks) {
mAssistHandleCallbacks = callbacks;
- NavigationBarFragment navigationBarFragment =
- Dependency.get(NavigationBarController.class).getDefaultNavigationBarFragment();
- mNavBarDisplayId = navigationBarFragment.mDisplayId;
- mIsNavBarWindowVisible = navigationBarFragment.isNavBarWindowVisible();
- mCommandQueue = SysUiServiceProvider.getComponent(context, CommandQueue.class);
- mCommandQueue.addCallback(mCallbacks);
+ mIsDozing = mStatusBarStateController.isDozing();
+ mStatusBarStateController.addCallback(mStatusBarStateListener);
+ mOverviewProxyService.addCallback(mOverviewProxyListener);
callbackForCurrentState();
}
@Override
public void onModeDeactivated() {
mAssistHandleCallbacks = null;
- mCommandQueue.removeCallback(mCallbacks);
+ mOverviewProxyService.removeCallback(mOverviewProxyListener);
}
- private void handleWindowStateChanged(int state) {
- boolean newVisibility = state == StatusBarManager.WINDOW_STATE_SHOWING;
- if (mIsNavBarWindowVisible == newVisibility) {
+ private static boolean isHomeHandleHiding(int sysuiStateFlags) {
+ return (sysuiStateFlags & QuickStepContract.SYSUI_STATE_NAV_BAR_HIDDEN) != 0;
+ }
+
+ private void handleDozingChanged(boolean isDozing) {
+ if (mIsDozing == isDozing) {
return;
}
- mIsNavBarWindowVisible = newVisibility;
+ mIsDozing = isDozing;
+ callbackForCurrentState();
+ }
+
+ private void handleSystemUiStateChange(int sysuiStateFlags) {
+ boolean isHomeHandleHiding = isHomeHandleHiding(sysuiStateFlags);
+ if (mIsHomeHandleHiding == isHomeHandleHiding) {
+ return;
+ }
+
+ mIsHomeHandleHiding = isHomeHandleHiding;
callbackForCurrentState();
}
@@ -83,10 +102,10 @@
return;
}
- if (mIsNavBarWindowVisible) {
- mAssistHandleCallbacks.showAndStay();
- } else {
+ if (mIsHomeHandleHiding || mIsDozing) {
mAssistHandleCallbacks.hide();
+ } else {
+ mAssistHandleCallbacks.showAndStay();
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/assist/AssistHandleOffBehavior.java b/packages/SystemUI/src/com/android/systemui/assist/AssistHandleOffBehavior.java
index ebf15ae..f4130ae 100644
--- a/packages/SystemUI/src/com/android/systemui/assist/AssistHandleOffBehavior.java
+++ b/packages/SystemUI/src/com/android/systemui/assist/AssistHandleOffBehavior.java
@@ -27,9 +27,4 @@
public void onModeActivated(Context context, AssistHandleCallbacks callbacks) {
callbacks.hide();
}
-
- @Override
- public void onModeDeactivated() {
- // Do nothing
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/assist/AssistHandleReminderExpBehavior.java b/packages/SystemUI/src/com/android/systemui/assist/AssistHandleReminderExpBehavior.java
index 4a5e0e8..4b6a6dc 100644
--- a/packages/SystemUI/src/com/android/systemui/assist/AssistHandleReminderExpBehavior.java
+++ b/packages/SystemUI/src/com/android/systemui/assist/AssistHandleReminderExpBehavior.java
@@ -16,24 +16,27 @@
package com.android.systemui.assist;
+import android.app.ActivityManager;
import android.content.ComponentName;
import android.content.Context;
-import android.graphics.Rect;
-import android.view.View;
-import android.view.WindowManager;
+import android.os.SystemClock;
+import android.provider.DeviceConfig;
+import android.provider.Settings;
import androidx.annotation.Nullable;
+import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
import com.android.systemui.Dependency;
-import com.android.systemui.SysUiServiceProvider;
import com.android.systemui.assist.AssistHandleBehaviorController.BehaviorController;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.recents.OverviewProxyService;
import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.shared.system.QuickStepContract;
import com.android.systemui.shared.system.TaskStackChangeListener;
-import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.StatusBarState;
+import java.util.concurrent.TimeUnit;
+
/**
* Assistant handle behavior that hides the handles when the phone is dozing or in immersive mode,
* shows the handles when on lockscreen, and shows the handles temporarily when changing tasks or
@@ -41,6 +44,11 @@
*/
final class AssistHandleReminderExpBehavior implements BehaviorController {
+ private static final String LEARNING_TIME_ELAPSED_KEY = "reminder_exp_learning_time_elapsed";
+ private static final String LEARNING_EVENT_COUNT_KEY = "reminder_exp_learning_event_count";
+ private static final long DEFAULT_LEARNING_TIME_MS = TimeUnit.DAYS.toMillis(3);
+ private static final int DEFAULT_LEARNING_COUNT = 3;
+
private final StatusBarStateController.StateListener mStatusBarStateListener =
new StatusBarStateController.StateListener() {
@Override
@@ -65,66 +73,95 @@
handleTaskStackTopChanged(taskId);
}
};
- private final CommandQueue.Callbacks mCallbacks = new CommandQueue.Callbacks() {
- @Override
- public void setSystemUiVisibility(int displayId, int vis,
- int fullscreenStackVis, int dockedStackVis, int mask,
- Rect fullscreenStackBounds, Rect dockedStackBounds,
- boolean navbarColorManagedByIme) {
- if (mStatusBarDisplayId == displayId) {
- handleSystemUiVisibilityChange(vis, mask);
- }
- }
- };
private final OverviewProxyService.OverviewProxyListener mOverviewProxyListener =
new OverviewProxyService.OverviewProxyListener() {
@Override
public void onOverviewShown(boolean fromHome) {
handleOverviewShown();
}
+
+ @Override
+ public void onSystemUiStateChanged(int sysuiStateFlags) {
+ handleSystemUiStateChanged(sysuiStateFlags);
+ }
};
- private StatusBarStateController mStatusBarStateController;
- private ActivityManagerWrapper mActivityManagerWrapper;
- private OverviewProxyService mOverviewProxyService;
- private int mStatusBarDisplayId;
- private CommandQueue mCommandQueue;
+ private final StatusBarStateController mStatusBarStateController;
+ private final ActivityManagerWrapper mActivityManagerWrapper;
+ private final OverviewProxyService mOverviewProxyService;
+
private boolean mOnLockscreen;
private boolean mIsDozing;
private int mRunningTaskId;
- private boolean mIsImmersive;
+ private boolean mIsNavBarHidden;
+ /** Whether user has learned the gesture. */
+ private boolean mIsLearned;
+ private long mLastLearningTimestamp;
+ /** Uptime while in this behavior. */
+ private long mLearningTimeElapsed;
+ /** Number of successful Assistant invocations while in this behavior. */
+ private int mLearningCount;
+
+ @Nullable private Context mContext;
@Nullable private AssistHandleCallbacks mAssistHandleCallbacks;
+ AssistHandleReminderExpBehavior() {
+ mStatusBarStateController = Dependency.get(StatusBarStateController.class);
+ mActivityManagerWrapper = ActivityManagerWrapper.getInstance();
+ mOverviewProxyService = Dependency.get(OverviewProxyService.class);
+ }
+
@Override
public void onModeActivated(Context context, AssistHandleCallbacks callbacks) {
+ mContext = context;
mAssistHandleCallbacks = callbacks;
- mStatusBarStateController = Dependency.get(StatusBarStateController.class);
mOnLockscreen = onLockscreen(mStatusBarStateController.getState());
mIsDozing = mStatusBarStateController.isDozing();
mStatusBarStateController.addCallback(mStatusBarStateListener);
- mActivityManagerWrapper = ActivityManagerWrapper.getInstance();
- mRunningTaskId = mActivityManagerWrapper.getRunningTask().taskId;
+ ActivityManager.RunningTaskInfo runningTaskInfo = mActivityManagerWrapper.getRunningTask();
+ mRunningTaskId = runningTaskInfo == null ? 0 : runningTaskInfo.taskId;
mActivityManagerWrapper.registerTaskStackListener(mTaskStackChangeListener);
- mStatusBarDisplayId =
- ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE))
- .getDefaultDisplay().getDisplayId();
- mCommandQueue = SysUiServiceProvider.getComponent(context, CommandQueue.class);
- mCommandQueue.addCallback(mCallbacks);
- mOverviewProxyService = Dependency.get(OverviewProxyService.class);
mOverviewProxyService.addCallback(mOverviewProxyListener);
- callbackForCurrentState();
+
+ mLearningTimeElapsed = Settings.Secure.getLong(
+ context.getContentResolver(), LEARNING_TIME_ELAPSED_KEY, /* default = */ 0);
+ mLearningCount = Settings.Secure.getInt(
+ context.getContentResolver(), LEARNING_EVENT_COUNT_KEY, /* default = */ 0);
+ mLastLearningTimestamp = SystemClock.uptimeMillis();
+
+ callbackForCurrentState(/* justUnlocked = */ false);
}
@Override
public void onModeDeactivated() {
mAssistHandleCallbacks = null;
+ if (mContext != null) {
+ Settings.Secure.putLong(
+ mContext.getContentResolver(), LEARNING_TIME_ELAPSED_KEY, mLearningTimeElapsed);
+ Settings.Secure.putInt(
+ mContext.getContentResolver(), LEARNING_EVENT_COUNT_KEY, mLearningCount);
+ mContext = null;
+ }
mStatusBarStateController.removeCallback(mStatusBarStateListener);
mActivityManagerWrapper.unregisterTaskStackListener(mTaskStackChangeListener);
- mCommandQueue.removeCallback(mCallbacks);
mOverviewProxyService.removeCallback(mOverviewProxyListener);
}
+ @Override
+ public void onAssistantGesturePerformed() {
+ if (mContext == null) {
+ return;
+ }
+
+ Settings.Secure.putLong(
+ mContext.getContentResolver(), LEARNING_EVENT_COUNT_KEY, ++mLearningCount);
+ }
+
+ private static boolean isNavBarHidden(int sysuiStateFlags) {
+ return (sysuiStateFlags & QuickStepContract.SYSUI_STATE_NAV_BAR_HIDDEN) != 0;
+ }
+
private void handleStatusBarStateChanged(int newState) {
boolean onLockscreen = onLockscreen(newState);
if (mOnLockscreen == onLockscreen) {
@@ -132,7 +169,7 @@
}
mOnLockscreen = onLockscreen;
- callbackForCurrentState();
+ callbackForCurrentState(!onLockscreen);
}
private void handleDozingChanged(boolean isDozing) {
@@ -141,7 +178,7 @@
}
mIsDozing = isDozing;
- callbackForCurrentState();
+ callbackForCurrentState(/* justUnlocked = */ false);
}
private void handleTaskStackTopChanged(int taskId) {
@@ -150,21 +187,21 @@
}
mRunningTaskId = taskId;
- callbackForCurrentState();
+ callbackForCurrentState(/* justUnlocked = */ false);
}
- private void handleSystemUiVisibilityChange(int vis, int mask) {
- boolean isImmersive = isImmersive(vis, mask);
- if (mIsImmersive == isImmersive) {
+ private void handleSystemUiStateChanged(int sysuiStateFlags) {
+ boolean isNavBarHidden = isNavBarHidden(sysuiStateFlags);
+ if (mIsNavBarHidden == isNavBarHidden) {
return;
}
- mIsImmersive = isImmersive;
- callbackForCurrentState();
+ mIsNavBarHidden = isNavBarHidden;
+ callbackForCurrentState(/* justUnlocked = */ false);
}
private void handleOverviewShown() {
- callbackForCurrentState();
+ callbackForCurrentState(/* justUnlocked = */ false);
}
private boolean onLockscreen(int statusBarState) {
@@ -172,17 +209,34 @@
|| statusBarState == StatusBarState.SHADE_LOCKED;
}
- private boolean isImmersive(int vis, int mask) {
- return ((vis & mask)
- & (View.SYSTEM_UI_FLAG_IMMERSIVE | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY)) != 0;
+ private void callbackForCurrentState(boolean justUnlocked) {
+ updateLearningStatus();
+
+ if (mIsLearned) {
+ callbackForLearnedState(justUnlocked);
+ } else {
+ callbackForUnlearnedState();
+ }
}
- private void callbackForCurrentState() {
+ private void callbackForLearnedState(boolean justUnlocked) {
if (mAssistHandleCallbacks == null) {
return;
}
- if (mIsDozing || mIsImmersive) {
+ if (mIsDozing || mIsNavBarHidden || mOnLockscreen) {
+ mAssistHandleCallbacks.hide();
+ } else if (justUnlocked) {
+ mAssistHandleCallbacks.showAndGo();
+ }
+ }
+
+ private void callbackForUnlearnedState() {
+ if (mAssistHandleCallbacks == null) {
+ return;
+ }
+
+ if (mIsDozing || mIsNavBarHidden) {
mAssistHandleCallbacks.hide();
} else if (mOnLockscreen) {
mAssistHandleCallbacks.showAndStay();
@@ -190,4 +244,33 @@
mAssistHandleCallbacks.showAndGo();
}
}
+
+ private void updateLearningStatus() {
+ if (mContext == null) {
+ return;
+ }
+
+ long currentTimestamp = SystemClock.uptimeMillis();
+ mLearningTimeElapsed += currentTimestamp - mLastLearningTimestamp;
+ mLastLearningTimestamp = currentTimestamp;
+ Settings.Secure.putLong(
+ mContext.getContentResolver(), LEARNING_TIME_ELAPSED_KEY, mLearningTimeElapsed);
+
+ mIsLearned =
+ mLearningCount >= getLearningCount() || mLearningTimeElapsed >= getLearningTimeMs();
+ }
+
+ private long getLearningTimeMs() {
+ return DeviceConfig.getLong(
+ DeviceConfig.NAMESPACE_SYSTEMUI,
+ SystemUiDeviceConfigFlags.ASSIST_HANDLES_LEARN_TIME_MS,
+ DEFAULT_LEARNING_TIME_MS);
+ }
+
+ private int getLearningCount() {
+ return DeviceConfig.getInt(
+ DeviceConfig.NAMESPACE_SYSTEMUI,
+ SystemUiDeviceConfigFlags.ASSIST_HANDLES_LEARN_COUNT,
+ DEFAULT_LEARNING_COUNT);
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java b/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java
index 2c38e51..2fc79d6 100644
--- a/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java
+++ b/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java
@@ -40,8 +40,11 @@
import com.android.keyguard.KeyguardUpdateMonitor;
import com.android.settingslib.applications.InterestingConfigChanges;
import com.android.systemui.ConfigurationChangedReceiver;
+import com.android.systemui.Dependency;
import com.android.systemui.R;
import com.android.systemui.SysUiServiceProvider;
+import com.android.systemui.assist.ui.DefaultUiController;
+import com.android.systemui.recents.OverviewProxyService;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.policy.DeviceProvisionedController;
@@ -50,6 +53,40 @@
*/
public class AssistManager implements ConfigurationChangedReceiver {
+ /**
+ * Controls the UI for showing Assistant invocation progress.
+ */
+ public interface UiController {
+ /**
+ * Updates the invocation progress.
+ *
+ * @param type one of INVOCATION_TYPE_GESTURE, INVOCATION_TYPE_ACTIVE_EDGE,
+ * INVOCATION_TYPE_VOICE, INVOCATION_TYPE_QUICK_SEARCH_BAR,
+ * INVOCATION_HOME_BUTTON_LONG_PRESS
+ * @param progress a float between 0 and 1 inclusive. 0 represents the beginning of the
+ * gesture; 1 represents the end.
+ */
+ void onInvocationProgress(int type, float progress);
+
+ /**
+ * Called when an invocation gesture completes.
+ *
+ * @param velocity the speed of the invocation gesture, in pixels per millisecond. For
+ * drags, this is 0.
+ */
+ void onGestureCompletion(float velocity);
+
+ /**
+ * Called with the Bundle from VoiceInteractionSessionListener.onSetUiHints.
+ */
+ void processBundle(Bundle hints);
+
+ /**
+ * Hides the UI.
+ */
+ void hide();
+ }
+
private static final String TAG = "AssistManager";
// Note that VERBOSE logging may leak PII (e.g. transcription contents).
@@ -76,6 +113,7 @@
private final InterestingConfigChanges mInterestingConfigChanges;
private final PhoneStateMonitor mPhoneStateMonitor;
private final AssistHandleBehaviorController mHandleController;
+ private final UiController mUiController;
private AssistOrbContainer mView;
private final DeviceProvisionedController mDeviceProvisionedController;
@@ -85,16 +123,16 @@
private IVoiceInteractionSessionShowCallback mShowCallback =
new IVoiceInteractionSessionShowCallback.Stub() {
- @Override
- public void onFailed() throws RemoteException {
- mView.post(mHideRunnable);
- }
+ @Override
+ public void onFailed() throws RemoteException {
+ mView.post(mHideRunnable);
+ }
- @Override
- public void onShown() throws RemoteException {
- mView.post(mHideRunnable);
- }
- };
+ @Override
+ public void onShown() throws RemoteException {
+ mView.post(mHideRunnable);
+ }
+ };
private Runnable mHideRunnable = new Runnable() {
@Override
@@ -111,7 +149,8 @@
mAssistUtils = new AssistUtils(context);
mAssistDisclosure = new AssistDisclosure(context, new Handler());
mPhoneStateMonitor = new PhoneStateMonitor(context);
- mHandleController = new AssistHandleBehaviorController(context, new Handler());
+ mHandleController =
+ new AssistHandleBehaviorController(context, mAssistUtils, new Handler());
registerVoiceInteractionSessionListener();
mInterestingConfigChanges = new InterestingConfigChanges(ActivityInfo.CONFIG_ORIENTATION
@@ -119,6 +158,23 @@
| ActivityInfo.CONFIG_SCREEN_LAYOUT | ActivityInfo.CONFIG_ASSETS_PATHS);
onConfigurationChanged(context.getResources().getConfiguration());
mShouldEnableOrb = !ActivityManager.isLowRamDeviceStatic();
+
+ mUiController = new DefaultUiController(mContext);
+
+ OverviewProxyService overviewProxy = Dependency.get(OverviewProxyService.class);
+ overviewProxy.addCallback(new OverviewProxyService.OverviewProxyListener() {
+ @Override
+ public void onAssistantProgress(float progress) {
+ // Progress goes from 0 to 1 to indicate how close the assist gesture is to
+ // completion.
+ onInvocationProgress(INVOCATION_TYPE_GESTURE, progress);
+ }
+
+ @Override
+ public void onAssistantGestureCompletion(float velocity) {
+ onGestureCompletion(velocity);
+ }
+ });
}
protected void registerVoiceInteractionSessionListener() {
@@ -191,26 +247,32 @@
if (args == null) {
args = new Bundle();
}
+ int invocationType = args.getInt(INVOCATION_TYPE_KEY, 0);
+ if (invocationType == INVOCATION_TYPE_GESTURE) {
+ mHandleController.onAssistantGesturePerformed();
+ }
args.putInt(INVOCATION_PHONE_STATE_KEY, mPhoneStateMonitor.getPhoneState());
args.putLong(INVOCATION_TIME_MS_KEY, SystemClock.uptimeMillis());
// Logs assistant start with invocation type.
MetricsLogger.action(
new LogMaker(MetricsEvent.ASSISTANT)
- .setType(MetricsEvent.TYPE_OPEN).setSubtype(args.getInt(INVOCATION_TYPE_KEY)));
+ .setType(MetricsEvent.TYPE_OPEN).setSubtype(
+ invocationType));
startAssistInternal(args, assistComponent, isService);
}
/** Called when the user is performing an assistant invocation action (e.g. Active Edge) */
public void onInvocationProgress(int type, float progress) {
- // intentional no-op, vendor's AssistManager implementation should override if needed.
+ mUiController.onInvocationProgress(type, progress);
}
- /** Called when the user has invoked the assistant with the incoming velocity, in pixels per
+ /**
+ * Called when the user has invoked the assistant with the incoming velocity, in pixels per
* millisecond. For invocations without a velocity (e.g. slow drag), the velocity is set to
* zero.
*/
- public void onAssistantGestureCompletion(float velocity) {
- // intentional no-op, vendor's AssistManager implementation should override if needed.
+ public void onGestureCompletion(float velocity) {
+ mUiController.onGestureCompletion(velocity);
}
public void hideAssist() {
@@ -264,7 +326,7 @@
Settings.Secure.ASSIST_STRUCTURE_ENABLED, 1, UserHandle.USER_CURRENT) != 0;
final SearchManager searchManager =
- (SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE);
+ (SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE);
if (searchManager == null) {
return;
}
@@ -329,7 +391,7 @@
// Look for the search icon specified in the activity meta-data
Bundle metaData = isService
? packageManager.getServiceInfo(
- component, PackageManager.GET_META_DATA).metaData
+ component, PackageManager.GET_META_DATA).metaData
: packageManager.getActivityInfo(
component, PackageManager.GET_META_DATA).metaData;
if (metaData != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/assist/ui/CircularCornerPathRenderer.java b/packages/SystemUI/src/com/android/systemui/assist/ui/CircularCornerPathRenderer.java
new file mode 100644
index 0000000..162e09e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/assist/ui/CircularCornerPathRenderer.java
@@ -0,0 +1,65 @@
+/*
+ * 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.assist.ui;
+
+import android.graphics.Path;
+
+/**
+ * Describes paths for circular rounded device corners.
+ */
+public final class CircularCornerPathRenderer extends CornerPathRenderer {
+
+ private final int mCornerRadiusBottom;
+ private final int mCornerRadiusTop;
+ private final int mHeight;
+ private final int mWidth;
+ private final Path mPath = new Path();
+
+ public CircularCornerPathRenderer(int cornerRadiusBottom, int cornerRadiusTop,
+ int width, int height) {
+ mCornerRadiusBottom = cornerRadiusBottom;
+ mCornerRadiusTop = cornerRadiusTop;
+ mHeight = height;
+ mWidth = width;
+ }
+
+ @Override // CornerPathRenderer
+ public Path getCornerPath(Corner corner) {
+ mPath.reset();
+ switch (corner) {
+ case BOTTOM_LEFT:
+ mPath.moveTo(0, mHeight - mCornerRadiusBottom);
+ mPath.arcTo(0, mHeight - mCornerRadiusBottom * 2, mCornerRadiusBottom * 2, mHeight,
+ 180, -90, true);
+ break;
+ case BOTTOM_RIGHT:
+ mPath.moveTo(mWidth - mCornerRadiusBottom, mHeight);
+ mPath.arcTo(mWidth - mCornerRadiusBottom * 2, mHeight - mCornerRadiusBottom * 2,
+ mWidth, mHeight, 90, -90, true);
+ break;
+ case TOP_RIGHT:
+ mPath.moveTo(mWidth, mCornerRadiusTop);
+ mPath.arcTo(mWidth - mCornerRadiusTop, 0, mWidth, mCornerRadiusTop, 0, -90, true);
+ break;
+ case TOP_LEFT:
+ mPath.moveTo(mCornerRadiusTop, 0);
+ mPath.arcTo(0, 0, mCornerRadiusTop, mCornerRadiusTop, 270, -90, true);
+ break;
+ }
+ return mPath;
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/assist/ui/CornerPathRenderer.java b/packages/SystemUI/src/com/android/systemui/assist/ui/CornerPathRenderer.java
new file mode 100644
index 0000000..2b40e65
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/assist/ui/CornerPathRenderer.java
@@ -0,0 +1,140 @@
+/*
+ * 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.assist.ui;
+
+import android.graphics.Path;
+import android.graphics.PointF;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Handles paths along device corners.
+ */
+public abstract class CornerPathRenderer {
+
+ // The maximum delta between the corner curve and points approximating the corner curve.
+ private static final float ACCEPTABLE_ERROR = 0.1f;
+
+ /**
+ * For convenience, labels the four device corners.
+ *
+ * Corners must be listed in CCW order, otherwise we'll break rotation.
+ */
+ public enum Corner {
+ BOTTOM_LEFT,
+ BOTTOM_RIGHT,
+ TOP_RIGHT,
+ TOP_LEFT
+ }
+
+ /**
+ * Returns the path along the inside of a corner (centered insetAmountPx from the corner's
+ * edge).
+ */
+ public Path getInsetPath(Corner corner, float insetAmountPx) {
+ return approximateInnerPath(getCornerPath(corner), -insetAmountPx);
+ }
+
+ /**
+ * Returns the path of a corner (centered on the exact corner). Must be implemented by extending
+ * classes, based on the device-specific rounded corners. A default implementation for circular
+ * corners is provided by CircularCornerPathRenderer.
+ */
+ public abstract Path getCornerPath(Corner corner);
+
+ private Path approximateInnerPath(Path input, float delta) {
+ List<PointF> points = shiftBy(getApproximatePoints(input), delta);
+ return toPath(points);
+ }
+
+ private ArrayList<PointF> getApproximatePoints(Path path) {
+ float[] rawInput = path.approximate(ACCEPTABLE_ERROR);
+
+ ArrayList<PointF> output = new ArrayList<>();
+
+ for (int i = 0; i < rawInput.length; i = i + 3) {
+ output.add(new PointF(rawInput[i + 1], rawInput[i + 2]));
+ }
+
+ return output;
+ }
+
+ private ArrayList<PointF> shiftBy(ArrayList<PointF> input, float delta) {
+ ArrayList<PointF> output = new ArrayList<>();
+
+ for (int i = 0; i < input.size(); i++) {
+ PointF point = input.get(i);
+ PointF normal = normalAt(input, i);
+ PointF shifted =
+ new PointF(point.x + (normal.x * delta), point.y + (normal.y * delta));
+ output.add(shifted);
+ }
+ return output;
+ }
+
+ private Path toPath(List<PointF> points) {
+ Path path = new Path();
+ if (points.size() > 0) {
+ path.moveTo(points.get(0).x, points.get(0).y);
+ for (PointF point : points.subList(1, points.size())) {
+ path.lineTo(point.x, point.y);
+ }
+ }
+ return path;
+ }
+
+ private PointF normalAt(List<PointF> points, int index) {
+ PointF d1;
+ if (index == 0) {
+ d1 = new PointF(0, 0);
+ } else {
+ PointF point = points.get(index);
+ PointF previousPoint = points.get(index - 1);
+ d1 = new PointF((point.x - previousPoint.x), (point.y - previousPoint.y));
+ }
+
+ PointF d2;
+ if (index == (points.size() - 1)) {
+ d2 = new PointF(0, 0);
+ } else {
+ PointF point = points.get(index);
+ PointF nextPoint = points.get(index + 1);
+ d2 = new PointF((nextPoint.x - point.x), (nextPoint.y - point.y));
+ }
+
+ return rotate90Ccw(normalize(new PointF(d1.x + d2.x, d1.y + d2.y)));
+ }
+
+ private PointF rotate90Ccw(PointF input) {
+ return new PointF(-input.y, input.x);
+ }
+
+ private float magnitude(PointF point) {
+ return (float) Math.sqrt((point.x * point.x) + (point.y * point.y));
+ }
+
+ private PointF normalize(PointF point) {
+ float magnitude = magnitude(point);
+ if (magnitude == 0.f) {
+ return point;
+ }
+
+ float normal = 1 / magnitude;
+ return new PointF((point.x * normal), (point.y * normal));
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/assist/ui/DefaultUiController.java b/packages/SystemUI/src/com/android/systemui/assist/ui/DefaultUiController.java
new file mode 100644
index 0000000..b1be811
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/assist/ui/DefaultUiController.java
@@ -0,0 +1,187 @@
+/*
+ * 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.assist.ui;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.annotation.ColorInt;
+import android.content.Context;
+import android.graphics.PixelFormat;
+import android.metrics.LogMaker;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.WindowManager;
+import android.view.animation.PathInterpolator;
+import android.widget.FrameLayout;
+
+import com.android.internal.logging.MetricsLogger;
+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;
+
+/**
+ * Default UiController implementation. Shows white edge lights along the bottom of the phone,
+ * expanding from the corners to meet in the center.
+ */
+public class DefaultUiController implements AssistManager.UiController {
+
+ private static final String TAG = "DefaultUiController";
+
+ private static final long ANIM_DURATION_MS = 200;
+
+ protected final FrameLayout mRoot;
+
+ private final WindowManager mWindowManager;
+ private final WindowManager.LayoutParams mLayoutParams;
+ private final PathInterpolator mProgressInterpolator = new PathInterpolator(.83f, 0, .84f, 1);
+
+ private boolean mAttached = false;
+ private boolean mInvocationInProgress = false;
+ private float mLastInvocationProgress = 0;
+
+ private ValueAnimator mInvocationAnimator = new ValueAnimator();
+ private InvocationLightsView mInvocationLightsView;
+
+ public DefaultUiController(Context context) {
+ mRoot = new FrameLayout(context);
+ mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+
+ mLayoutParams = new WindowManager.LayoutParams(
+ WindowManager.LayoutParams.MATCH_PARENT,
+ WindowManager.LayoutParams.WRAP_CONTENT, 0, 0,
+ WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
+ WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
+ | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
+ | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
+ | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
+ PixelFormat.TRANSLUCENT);
+ mLayoutParams.privateFlags = WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION;
+ mLayoutParams.gravity = Gravity.BOTTOM;
+ mLayoutParams.setTitle("Assist");
+
+ mInvocationLightsView = (InvocationLightsView)
+ LayoutInflater.from(context).inflate(R.layout.invocation_lights, mRoot, false);
+ mRoot.addView(mInvocationLightsView);
+ }
+
+ @Override // AssistManager.UiController
+ public void processBundle(Bundle bundle) {
+ Log.e(TAG, "Bundle received but handling is not implemented; ignoring");
+ }
+
+ @Override // AssistManager.UiController
+ public void onInvocationProgress(int type, float progress) {
+ if (progress == 1) {
+ animateInvocationCompletion(type, 0);
+ } else if (progress == 0) {
+ hide();
+ } else {
+ if (!mInvocationInProgress) {
+ attach();
+ mInvocationInProgress = true;
+ updateAssistHandleVisibility();
+ }
+ setProgressInternal(type, progress);
+ }
+ mLastInvocationProgress = progress;
+
+ // Logs assistant invocation start.
+ if (!mInvocationInProgress && progress > 0.f) {
+ MetricsLogger.action(new LogMaker(MetricsEvent.ASSISTANT)
+ .setType(MetricsEvent.TYPE_ACTION));
+ }
+ // Logs assistant invocation cancelled.
+ if (mInvocationInProgress && progress == 0f) {
+ MetricsLogger.action(new LogMaker(MetricsEvent.ASSISTANT)
+ .setType(MetricsEvent.TYPE_DISMISS).setSubtype(0));
+ }
+ }
+
+ @Override // AssistManager.UiController
+ public void onGestureCompletion(float velocity) {
+ animateInvocationCompletion(AssistManager.INVOCATION_TYPE_GESTURE, velocity);
+ }
+
+ @Override // AssistManager.UiController
+ public void hide() {
+ Dependency.get(AssistManager.class).hideAssist();
+ detach();
+ if (mInvocationAnimator.isRunning()) {
+ mInvocationAnimator.cancel();
+ }
+ mInvocationLightsView.hide();
+ mInvocationInProgress = false;
+ updateAssistHandleVisibility();
+ }
+
+ /**
+ * Sets the colors of the four invocation lights, from left to right.
+ */
+ public void setInvocationColors(@ColorInt int color1, @ColorInt int color2,
+ @ColorInt int color3, @ColorInt int color4) {
+ 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);
+ mAttached = true;
+ }
+ }
+
+ private void detach() {
+ if (mAttached) {
+ mWindowManager.removeViewImmediate(mRoot);
+ mAttached = false;
+ }
+ }
+
+ private void setProgressInternal(int type, float progress) {
+ mInvocationLightsView.onInvocationProgress(
+ mProgressInterpolator.getInterpolation(progress));
+ }
+
+ private void animateInvocationCompletion(int type, float velocity) {
+ mInvocationAnimator = ValueAnimator.ofFloat(mLastInvocationProgress, 1);
+ mInvocationAnimator.setStartDelay(1);
+ mInvocationAnimator.setDuration(ANIM_DURATION_MS);
+ mInvocationAnimator.addUpdateListener(
+ animation -> setProgressInternal(type, (float) animation.getAnimatedValue()));
+ mInvocationAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ super.onAnimationEnd(animation);
+ mInvocationInProgress = false;
+ mLastInvocationProgress = 0;
+ hide();
+ }
+ });
+ mInvocationAnimator.start();
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/assist/ui/DisplayUtils.java b/packages/SystemUI/src/com/android/systemui/assist/ui/DisplayUtils.java
new file mode 100644
index 0000000..251229f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/assist/ui/DisplayUtils.java
@@ -0,0 +1,128 @@
+/*
+ * 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.assist.ui;
+
+import android.content.Context;
+import android.util.DisplayMetrics;
+import android.view.Display;
+import android.view.Surface;
+
+/**
+ * Utility class for determining screen and corner dimensions.
+ */
+public class DisplayUtils {
+ /**
+ * Converts given distance from dp to pixels.
+ */
+ public static int convertDpToPx(float dp, Context context) {
+ Display d = context.getDisplay();
+
+ DisplayMetrics dm = new DisplayMetrics();
+ d.getRealMetrics(dm);
+
+ return (int) Math.ceil(dp * dm.density);
+ }
+
+ /**
+ * The width of the display.
+ *
+ * - Not affected by rotation.
+ * - Includes system decor.
+ */
+ public static int getWidth(Context context) {
+ Display d = context.getDisplay();
+
+ DisplayMetrics dm = new DisplayMetrics();
+ d.getRealMetrics(dm);
+
+ int rotation = d.getRotation();
+ if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) {
+ return dm.widthPixels;
+ } else {
+ return dm.heightPixels;
+ }
+ }
+
+ /**
+ * The height of the display.
+ *
+ * - Not affected by rotation.
+ * - Includes system decor.
+ */
+ public static int getHeight(Context context) {
+ Display d = context.getDisplay();
+
+ DisplayMetrics dm = new DisplayMetrics();
+ d.getRealMetrics(dm);
+
+ int rotation = d.getRotation();
+ if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) {
+ return dm.heightPixels;
+ } else {
+ return dm.widthPixels;
+ }
+ }
+
+ /**
+ * Returns the radius of the bottom corners (the distance from the true corner to the point
+ * where the curve ends), in pixels.
+ */
+ public static int getCornerRadiusBottom(Context context) {
+ int radius = 0;
+
+ int resourceId = context.getResources().getIdentifier("rounded_corner_radius_bottom",
+ "dimen", "android");
+ if (resourceId > 0) {
+ radius = context.getResources().getDimensionPixelSize(resourceId);
+ }
+
+ if (radius == 0) {
+ radius = getCornerRadiusDefault(context);
+ }
+ return radius;
+ }
+
+ /**
+ * Returns the radius of the top corners (the distance from the true corner to the point where
+ * the curve ends), in pixels.
+ */
+ public static int getCornerRadiusTop(Context context) {
+ int radius = 0;
+
+ int resourceId = context.getResources().getIdentifier("rounded_corner_radius_top",
+ "dimen", "android");
+ if (resourceId > 0) {
+ radius = context.getResources().getDimensionPixelSize(resourceId);
+ }
+
+ if (radius == 0) {
+ radius = getCornerRadiusDefault(context);
+ }
+ return radius;
+ }
+
+ private static int getCornerRadiusDefault(Context context) {
+ int radius = 0;
+
+ int resourceId = context.getResources().getIdentifier("rounded_corner_radius", "dimen",
+ "android");
+ if (resourceId > 0) {
+ radius = context.getResources().getDimensionPixelSize(resourceId);
+ }
+ return radius;
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/assist/ui/EdgeLight.java b/packages/SystemUI/src/com/android/systemui/assist/ui/EdgeLight.java
new file mode 100644
index 0000000..9ae02c5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/assist/ui/EdgeLight.java
@@ -0,0 +1,99 @@
+/*
+ * 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.assist.ui;
+
+import androidx.annotation.ColorInt;
+
+/**
+ * Represents a line drawn on the perimeter of the display.
+ *
+ * Offsets and lengths are both normalized to the perimeter of the display – ex. a length of 1
+ * is equal to the perimeter of the display. Positions move counter-clockwise as values increase.
+ *
+ * If there is no bottom corner radius, the origin is the bottom-left corner.
+ * If there is a bottom corner radius, the origin is immediately after the bottom corner radius,
+ * counter-clockwise.
+ */
+public final class EdgeLight {
+ @ColorInt
+ private int mColor;
+ private float mOffset;
+ private float mLength;
+
+ /** Copies a list of EdgeLights. */
+ public static EdgeLight[] copy(EdgeLight[] array) {
+ EdgeLight[] copy = new EdgeLight[array.length];
+ for (int i = 0; i < array.length; i++) {
+ copy[i] = new EdgeLight(array[i]);
+ }
+ return copy;
+ }
+
+ public EdgeLight(@ColorInt int color, float offset, float length) {
+ mColor = color;
+ mOffset = offset;
+ mLength = length;
+ }
+
+ public EdgeLight(EdgeLight sourceLight) {
+ mColor = sourceLight.getColor();
+ mOffset = sourceLight.getOffset();
+ mLength = sourceLight.getLength();
+ }
+
+ /** Returns the current edge light color. */
+ @ColorInt
+ public int getColor() {
+ return mColor;
+ }
+
+ /** Sets the edge light color. */
+ public void setColor(@ColorInt int color) {
+ mColor = color;
+ }
+
+ /** Returns the edge light length, in units of the total device perimeter. */
+ public float getLength() {
+ return mLength;
+ }
+
+ /** Sets the edge light length, in units of the total device perimeter. */
+ public void setLength(float length) {
+ mLength = length;
+ }
+
+ /**
+ * Returns the current offset, in units of the total device perimeter and measured from the
+ * bottom-left corner (see class description).
+ */
+ public float getOffset() {
+ return mOffset;
+ }
+
+ /**
+ * Sets the current offset, in units of the total device perimeter and measured from the
+ * bottom-left corner (see class description).
+ */
+ public void setOffset(float offset) {
+ mOffset = offset;
+ }
+
+ /** Returns the center, measured from the bottom-left corner (see class description). */
+ public float getCenter() {
+ return mOffset + (mLength / 2.f);
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/assist/ui/InvocationLightsView.java b/packages/SystemUI/src/com/android/systemui/assist/ui/InvocationLightsView.java
new file mode 100644
index 0000000..de1d7c8
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/assist/ui/InvocationLightsView.java
@@ -0,0 +1,194 @@
+/*
+ * 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.assist.ui;
+
+import android.annotation.ColorInt;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.MathUtils;
+import android.view.View;
+
+import com.android.systemui.R;
+
+import java.util.ArrayList;
+
+/**
+ * Shows lights at the bottom of the phone, marking the invocation progress.
+ */
+public class InvocationLightsView extends View {
+
+ private static final String TAG = "InvocationLightsView";
+
+ private static final int LIGHT_HEIGHT_DP = 3;
+ // minimum light length as a fraction of the corner length
+ private static final float MINIMUM_CORNER_RATIO = .6f;
+
+ protected final ArrayList<EdgeLight> mAssistInvocationLights = new ArrayList<>();
+ protected final PerimeterPathGuide mGuide;
+
+ private final Paint mPaint = new Paint();
+ // Path used to render lights. One instance is used to draw all lights and is cached to avoid
+ // allocation on each frame.
+ private final Path mPath = new Path();
+ private final int mViewHeight;
+
+ // Allocate variable for screen location lookup to avoid memory alloc onDraw()
+ private int[] mScreenLocation = new int[2];
+
+ public InvocationLightsView(Context context) {
+ this(context, null);
+ }
+
+ public InvocationLightsView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public InvocationLightsView(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public InvocationLightsView(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ int strokeWidth = DisplayUtils.convertDpToPx(LIGHT_HEIGHT_DP, context);
+ mPaint.setStrokeWidth(strokeWidth);
+ mPaint.setStyle(Paint.Style.STROKE);
+ mPaint.setStrokeJoin(Paint.Join.MITER);
+ mPaint.setAntiAlias(true);
+
+ int cornerRadiusBottom = DisplayUtils.getCornerRadiusBottom(context);
+ int cornerRadiusTop = DisplayUtils.getCornerRadiusTop(context);
+ int displayWidth = DisplayUtils.getWidth(context);
+ int displayHeight = DisplayUtils.getHeight(context);
+ CircularCornerPathRenderer cornerPathRenderer = new CircularCornerPathRenderer(
+ cornerRadiusBottom, cornerRadiusTop, displayWidth, displayHeight);
+ mGuide = new PerimeterPathGuide(context, cornerPathRenderer,
+ strokeWidth / 2, displayWidth, displayHeight);
+
+ mViewHeight = Math.max(cornerRadiusBottom, cornerRadiusTop);
+
+ @ColorInt int lightColor = getResources().getColor(R.color.default_invocation_lights_color);
+ for (int i = 0; i < 4; i++) {
+ mAssistInvocationLights.add(new EdgeLight(lightColor, 0, 0));
+ }
+ }
+
+ /**
+ * Updates positions of the invocation lights based on the progress (a float between 0 and 1).
+ * The lights begin at the device corners and expand inward until they meet at the center.
+ */
+ public void onInvocationProgress(float progress) {
+ if (progress == 0) {
+ setVisibility(View.GONE);
+ } else {
+ float cornerLengthNormalized =
+ mGuide.getRegionWidth(PerimeterPathGuide.Region.BOTTOM_LEFT);
+ float arcLengthNormalized = cornerLengthNormalized * MINIMUM_CORNER_RATIO;
+ float arcOffsetNormalized = (cornerLengthNormalized - arcLengthNormalized) / 2f;
+
+ float minLightLength = arcLengthNormalized / 2;
+ float maxLightLength = mGuide.getRegionWidth(PerimeterPathGuide.Region.BOTTOM) / 4f;
+
+ float lightLength = MathUtils.lerp(minLightLength, maxLightLength, progress);
+
+ float leftStart = (-cornerLengthNormalized + arcOffsetNormalized) * (1 - progress);
+ float rightStart = mGuide.getRegionWidth(PerimeterPathGuide.Region.BOTTOM)
+ + (cornerLengthNormalized - arcOffsetNormalized) * (1 - progress);
+
+ setLight(0, leftStart, lightLength);
+ setLight(1, leftStart + lightLength, lightLength);
+ setLight(2, rightStart - (lightLength * 2), lightLength);
+ setLight(3, rightStart - lightLength, lightLength);
+ setVisibility(View.VISIBLE);
+ }
+ invalidate();
+ }
+
+ /**
+ * Hides and resets the invocation lights.
+ */
+ public void hide() {
+ setVisibility(GONE);
+ for (EdgeLight light : mAssistInvocationLights) {
+ light.setLength(0);
+ }
+ }
+
+ /**
+ * Sets the invocation light colors, from left to right.
+ */
+ public void setColors(@ColorInt int color1, @ColorInt int color2,
+ @ColorInt int color3, @ColorInt int color4) {
+ mAssistInvocationLights.get(0).setColor(color1);
+ mAssistInvocationLights.get(1).setColor(color2);
+ mAssistInvocationLights.get(2).setColor(color3);
+ mAssistInvocationLights.get(3).setColor(color4);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ getLayoutParams().height = mViewHeight;
+ requestLayout();
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+
+ int rotation = getContext().getDisplay().getRotation();
+ mGuide.setRotation(rotation);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ // If the view doesn't take up the whole screen, offset the canvas by its translation
+ // distance such that PerimeterPathGuide's paths are drawn properly based upon the actual
+ // screen edges.
+ getLocationOnScreen(mScreenLocation);
+ canvas.translate(-mScreenLocation[0], -mScreenLocation[1]);
+
+ // if the lights are different colors, the inner ones need to be drawn last and with a
+ // square cap so that the join between lights is straight
+ mPaint.setStrokeCap(Paint.Cap.ROUND);
+ renderLight(mAssistInvocationLights.get(0), canvas);
+ renderLight(mAssistInvocationLights.get(3), canvas);
+
+ mPaint.setStrokeCap(Paint.Cap.SQUARE);
+ renderLight(mAssistInvocationLights.get(1), canvas);
+ renderLight(mAssistInvocationLights.get(2), canvas);
+ }
+
+ protected void setLight(int index, float offset, float length) {
+ if (index < 0 || index >= 4) {
+ Log.w(TAG, "invalid invocation light index: " + index);
+ }
+ mAssistInvocationLights.get(index).setOffset(offset);
+ mAssistInvocationLights.get(index).setLength(length);
+ }
+
+ private void renderLight(EdgeLight light, Canvas canvas) {
+ mGuide.strokeSegment(mPath, light.getOffset(), light.getOffset() + light.getLength());
+ mPaint.setColor(light.getColor());
+ canvas.drawPath(mPath, mPaint);
+ }
+
+}
diff --git a/packages/SystemUI/src/com/android/systemui/assist/ui/PerimeterPathGuide.java b/packages/SystemUI/src/com/android/systemui/assist/ui/PerimeterPathGuide.java
new file mode 100644
index 0000000..8eea368
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/assist/ui/PerimeterPathGuide.java
@@ -0,0 +1,389 @@
+/*
+ * 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.assist.ui;
+
+import static android.view.Surface.ROTATION_0;
+import static android.view.Surface.ROTATION_180;
+import static android.view.Surface.ROTATION_270;
+import static android.view.Surface.ROTATION_90;
+
+import android.content.Context;
+import android.graphics.Matrix;
+import android.graphics.Path;
+import android.graphics.PathMeasure;
+import android.util.Log;
+import android.util.Pair;
+import android.view.Surface;
+
+import androidx.core.math.MathUtils;
+
+/**
+ * PerimeterPathGuide establishes a coordinate system for drawing paths along the perimeter of the
+ * screen. All positions around the perimeter have a coordinate [0, 1). The origin is the bottom
+ * left corner of the screen, to the right of the curved corner, if any. Coordinates increase
+ * counter-clockwise around the screen.
+ *
+ * Non-square screens require PerimeterPathGuide to be notified when the rotation changes, such that
+ * it can recompute the edge lengths for the coordinate system.
+ */
+public class PerimeterPathGuide {
+
+ private static final String TAG = "PerimeterPathGuide";
+
+ /**
+ * For convenience, labels sections of the device perimeter.
+ *
+ * Must be listed in CCW order.
+ */
+ public enum Region {
+ BOTTOM,
+ BOTTOM_RIGHT,
+ RIGHT,
+ TOP_RIGHT,
+ TOP,
+ TOP_LEFT,
+ LEFT,
+ BOTTOM_LEFT
+ }
+
+ private final int mDeviceWidthPx;
+ private final int mDeviceHeightPx;
+ private final int mTopCornerRadiusPx;
+ private final int mBottomCornerRadiusPx;
+
+ private class RegionAttributes {
+ public float absoluteLength;
+ public float normalizedLength;
+ public float endCoordinate;
+ public Path path;
+ }
+
+ // Allocate a Path and PathMeasure for use by intermediate operations that would otherwise have
+ // to allocate. reset() must be called before using this path, this ensures state from previous
+ // operations is cleared.
+ private final Path mScratchPath = new Path();
+ private final CornerPathRenderer mCornerPathRenderer;
+ private final PathMeasure mScratchPathMeasure = new PathMeasure(mScratchPath, false);
+ private RegionAttributes[] mRegions;
+ private final int mEdgeInset;
+ private int mRotation = ROTATION_0;
+
+ public PerimeterPathGuide(Context context, CornerPathRenderer cornerPathRenderer,
+ int edgeInset, int screenWidth, int screenHeight) {
+ mCornerPathRenderer = cornerPathRenderer;
+ mDeviceWidthPx = screenWidth;
+ mDeviceHeightPx = screenHeight;
+ mTopCornerRadiusPx = DisplayUtils.getCornerRadiusTop(context);
+ mBottomCornerRadiusPx = DisplayUtils.getCornerRadiusBottom(context);
+ mEdgeInset = edgeInset;
+
+ mRegions = new RegionAttributes[8];
+ for (int i = 0; i < mRegions.length; i++) {
+ mRegions[i] = new RegionAttributes();
+ }
+ computeRegions();
+ }
+
+ /**
+ * Sets the rotation.
+ *
+ * @param rotation one of Surface.ROTATION_0, Surface.ROTATION_90, Surface.ROTATION_180,
+ * Surface.ROTATION_270
+ */
+ public void setRotation(int rotation) {
+ if (rotation != mRotation) {
+ switch (rotation) {
+ case ROTATION_0:
+ case ROTATION_90:
+ case ROTATION_180:
+ case ROTATION_270:
+ mRotation = rotation;
+ computeRegions();
+ break;
+ default:
+ Log.e(TAG, "Invalid rotation provided: " + rotation);
+ }
+ }
+ }
+
+ /**
+ * Sets path to the section of the perimeter between startCoord and endCoord (measured
+ * counter-clockwise from the bottom left).
+ */
+ public void strokeSegment(Path path, float startCoord, float endCoord) {
+ path.reset();
+
+ startCoord = ((startCoord % 1) + 1) % 1; // Wrap to the range [0, 1).
+ endCoord = ((endCoord % 1) + 1) % 1; // Wrap to the range [0, 1).
+ boolean outOfOrder = startCoord > endCoord;
+
+ if (outOfOrder) {
+ strokeSegmentInternal(path, startCoord, 1f);
+ startCoord = 0;
+ }
+ strokeSegmentInternal(path, startCoord, endCoord);
+ }
+
+ /**
+ * Returns the device perimeter in pixels.
+ */
+ public float getPerimeterPx() {
+ float total = 0;
+ for (RegionAttributes region : mRegions) {
+ total += region.absoluteLength;
+ }
+ return total;
+ }
+
+ /**
+ * Returns the bottom corner radius in pixels.
+ */
+ public float getBottomCornerRadiusPx() {
+ return mBottomCornerRadiusPx;
+ }
+
+ /**
+ * Given a region and a progress value [0,1] indicating the counter-clockwise progress within
+ * that region, compute the global [0,1) coordinate.
+ */
+ public float getCoord(Region region, float progress) {
+ RegionAttributes regionAttributes = mRegions[region.ordinal()];
+ progress = MathUtils.clamp(progress, 0, 1);
+ return regionAttributes.endCoordinate - (1 - progress) * regionAttributes.normalizedLength;
+ }
+
+ /**
+ * Returns the center of the provided region, relative to the entire perimeter.
+ */
+ public float getRegionCenter(Region region) {
+ return getCoord(region, 0.5f);
+ }
+
+ /**
+ * Returns the width of the provided region, in units relative to the entire perimeter.
+ */
+ public float getRegionWidth(Region region) {
+ return mRegions[region.ordinal()].normalizedLength;
+ }
+
+ /**
+ * Points are expressed in terms of their relative position on the perimeter of the display,
+ * moving counter-clockwise. This method converts a point to clockwise, assisting use cases
+ * such as animating to a point clockwise instead of counter-clockwise.
+ *
+ * @param point A point in the range from 0 to 1.
+ * @return A point in the range of -1 to 0 that represents the same location as {@code point}.
+ */
+ public static float makeClockwise(float point) {
+ return point - 1;
+ }
+
+ private int getPhysicalCornerRadius(CircularCornerPathRenderer.Corner corner) {
+ if (corner == CircularCornerPathRenderer.Corner.BOTTOM_LEFT
+ || corner == CircularCornerPathRenderer.Corner.BOTTOM_RIGHT) {
+ return mBottomCornerRadiusPx;
+ }
+ return mTopCornerRadiusPx;
+ }
+
+ // Populate mRegions based upon the current rotation value.
+ private void computeRegions() {
+ int screenWidth = mDeviceWidthPx;
+ int screenHeight = mDeviceHeightPx;
+
+ int rotateMatrix = 0;
+
+ switch (mRotation) {
+ case ROTATION_90:
+ rotateMatrix = -90;
+ break;
+ case ROTATION_180:
+ rotateMatrix = -180;
+ break;
+ case Surface.ROTATION_270:
+ rotateMatrix = -270;
+ break;
+ }
+
+ Matrix matrix = new Matrix();
+ matrix.postRotate(rotateMatrix, mDeviceWidthPx / 2, mDeviceHeightPx / 2);
+
+ if (mRotation == ROTATION_90 || mRotation == Surface.ROTATION_270) {
+ screenHeight = mDeviceWidthPx;
+ screenWidth = mDeviceHeightPx;
+ matrix.postTranslate((mDeviceHeightPx
+ - mDeviceWidthPx) / 2, (mDeviceWidthPx - mDeviceHeightPx) / 2);
+ }
+
+ CircularCornerPathRenderer.Corner screenBottomLeft = getRotatedCorner(
+ CircularCornerPathRenderer.Corner.BOTTOM_LEFT);
+ CircularCornerPathRenderer.Corner screenBottomRight = getRotatedCorner(
+ CircularCornerPathRenderer.Corner.BOTTOM_RIGHT);
+ CircularCornerPathRenderer.Corner screenTopLeft = getRotatedCorner(
+ CircularCornerPathRenderer.Corner.TOP_LEFT);
+ CircularCornerPathRenderer.Corner screenTopRight = getRotatedCorner(
+ CircularCornerPathRenderer.Corner.TOP_RIGHT);
+
+ mRegions[Region.BOTTOM_LEFT.ordinal()].path =
+ mCornerPathRenderer.getInsetPath(screenBottomLeft, mEdgeInset);
+ mRegions[Region.BOTTOM_RIGHT.ordinal()].path =
+ mCornerPathRenderer.getInsetPath(screenBottomRight, mEdgeInset);
+ mRegions[Region.TOP_RIGHT.ordinal()].path =
+ mCornerPathRenderer.getInsetPath(screenTopRight, mEdgeInset);
+ mRegions[Region.TOP_LEFT.ordinal()].path =
+ mCornerPathRenderer.getInsetPath(screenTopLeft, mEdgeInset);
+
+ mRegions[Region.BOTTOM_LEFT.ordinal()].path.transform(matrix);
+ mRegions[Region.BOTTOM_RIGHT.ordinal()].path.transform(matrix);
+ mRegions[Region.TOP_RIGHT.ordinal()].path.transform(matrix);
+ mRegions[Region.TOP_LEFT.ordinal()].path.transform(matrix);
+
+
+ Path bottomPath = new Path();
+ bottomPath.moveTo(getPhysicalCornerRadius(screenBottomLeft), screenHeight - mEdgeInset);
+ bottomPath.lineTo(screenWidth - getPhysicalCornerRadius(screenBottomRight),
+ screenHeight - mEdgeInset);
+ mRegions[Region.BOTTOM.ordinal()].path = bottomPath;
+
+ Path topPath = new Path();
+ topPath.moveTo(screenWidth - getPhysicalCornerRadius(screenTopRight), mEdgeInset);
+ topPath.lineTo(getPhysicalCornerRadius(screenTopLeft), mEdgeInset);
+ mRegions[Region.TOP.ordinal()].path = topPath;
+
+ Path rightPath = new Path();
+ rightPath.moveTo(screenWidth - mEdgeInset,
+ screenHeight - getPhysicalCornerRadius(screenBottomRight));
+ rightPath.lineTo(screenWidth - mEdgeInset, getPhysicalCornerRadius(screenTopRight));
+ mRegions[Region.RIGHT.ordinal()].path = rightPath;
+
+ Path leftPath = new Path();
+ leftPath.moveTo(mEdgeInset,
+ getPhysicalCornerRadius(screenTopLeft));
+ leftPath.lineTo(mEdgeInset, screenHeight - getPhysicalCornerRadius(screenBottomLeft));
+ mRegions[Region.LEFT.ordinal()].path = leftPath;
+
+ float perimeterLength = 0;
+ PathMeasure pathMeasure = new PathMeasure();
+ for (int i = 0; i < mRegions.length; i++) {
+ pathMeasure.setPath(mRegions[i].path, false);
+ mRegions[i].absoluteLength = pathMeasure.getLength();
+ perimeterLength += mRegions[i].absoluteLength;
+ }
+
+ float accum = 0;
+ for (int i = 0; i < mRegions.length; i++) {
+ mRegions[i].normalizedLength = mRegions[i].absoluteLength / perimeterLength;
+ accum += mRegions[i].normalizedLength;
+ mRegions[i].endCoordinate = accum;
+ }
+ }
+
+ private CircularCornerPathRenderer.Corner getRotatedCorner(
+ CircularCornerPathRenderer.Corner screenCorner) {
+ int corner = screenCorner.ordinal();
+ switch (mRotation) {
+ case ROTATION_90:
+ corner += 3;
+ break;
+ case ROTATION_180:
+ corner += 2;
+ break;
+ case Surface.ROTATION_270:
+ corner += 1;
+ break;
+ }
+ return CircularCornerPathRenderer.Corner.values()[corner % 4];
+ }
+
+ private void strokeSegmentInternal(Path path, float startCoord, float endCoord) {
+ Pair<Region, Float> startPoint = placePoint(startCoord);
+ Pair<Region, Float> endPoint = placePoint(endCoord);
+
+ if (startPoint.first.equals(endPoint.first)) {
+ strokeRegion(path, startPoint.first, startPoint.second, endPoint.second);
+ } else {
+ strokeRegion(path, startPoint.first, startPoint.second, 1f);
+ boolean hitStart = false;
+ for (Region r : Region.values()) {
+ if (r.equals(startPoint.first)) {
+ hitStart = true;
+ continue;
+ }
+ if (hitStart) {
+ if (!r.equals(endPoint.first)) {
+ strokeRegion(path, r, 0f, 1f);
+ } else {
+ strokeRegion(path, r, 0f, endPoint.second);
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ private void strokeRegion(Path path, Region r, float relativeStart, float relativeEnd) {
+ if (relativeStart == relativeEnd) {
+ return;
+ }
+
+ mScratchPathMeasure.setPath(mRegions[r.ordinal()].path, false);
+ mScratchPathMeasure.getSegment(relativeStart * mScratchPathMeasure.getLength(),
+ relativeEnd * mScratchPathMeasure.getLength(), path, true);
+ }
+
+ /**
+ * Return the Region where the point is located, and its relative position within that region
+ * (from 0 to 1).
+ * Note that we move counterclockwise around the perimeter; for example, a relative position of
+ * 0 in
+ * the BOTTOM region is on the left side of the screen, but in the TOP region it’s on the
+ * right.
+ */
+ private Pair<Region, Float> placePoint(float coord) {
+ if (0 > coord || coord > 1) {
+ coord = ((coord % 1) + 1)
+ % 1; // Wrap to the range [0, 1). Inputs of exactly 1 are preserved.
+ }
+
+ Region r = getRegionForPoint(coord);
+ if (r.equals(Region.BOTTOM)) {
+ return Pair.create(r, coord / mRegions[r.ordinal()].normalizedLength);
+ } else {
+ float coordOffsetInRegion = coord - mRegions[r.ordinal() - 1].endCoordinate;
+ float coordRelativeToRegion =
+ coordOffsetInRegion / mRegions[r.ordinal()].normalizedLength;
+ return Pair.create(r, coordRelativeToRegion);
+ }
+ }
+
+ private Region getRegionForPoint(float coord) {
+ // If coord is outside of [0,1], wrap to [0,1).
+ if (coord < 0 || coord > 1) {
+ coord = ((coord % 1) + 1) % 1;
+ }
+
+ for (Region region : Region.values()) {
+ if (coord <= mRegions[region.ordinal()].endCoordinate) {
+ return region;
+ }
+ }
+
+ // Should never happen.
+ Log.e(TAG, "Fell out of getRegionForPoint");
+ return Region.BOTTOM;
+ }
+}
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/collection/NotificationData.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationData.java
index 04ff58b..64b2f04 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationData.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationData.java
@@ -25,6 +25,7 @@
import android.service.notification.SnoozeCriterion;
import android.service.notification.StatusBarNotification;
import android.util.ArrayMap;
+import android.util.Slog;
import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.Dependency;
@@ -69,7 +70,8 @@
mHeadsUpManager = headsUpManager;
}
- private final Comparator<NotificationEntry> mRankingComparator =
+ @VisibleForTesting
+ protected final Comparator<NotificationEntry> mRankingComparator =
new Comparator<NotificationEntry>() {
private final Ranking mRankingA = new Ranking();
private final Ranking mRankingB = new Ranking();
@@ -120,6 +122,8 @@
} else if (aSystemMax != bSystemMax) {
// Upsort PRIORITY_MAX system notifications
return aSystemMax ? -1 : 1;
+ } else if (a.isHighPriority() != b.isHighPriority()) {
+ return -1 * Boolean.compare(a.isHighPriority(), b.isHighPriority());
} else if (aRank != bRank) {
return aRank - bRank;
} else {
@@ -231,17 +235,14 @@
/**
* Returns true if this notification should be displayed in the high-priority notifications
- * section (and on the lockscreen and status bar).
+ * section
*/
public boolean isHighPriority(StatusBarNotification statusBarNotification) {
if (mRankingMap != null) {
getRanking(statusBarNotification.getKey(), mTmpRanking);
if (mTmpRanking.getImportance() >= NotificationManager.IMPORTANCE_DEFAULT
- || isImportantOngoing(statusBarNotification.getNotification())
- || statusBarNotification.getNotification().hasMediaSession()
- || hasPerson(statusBarNotification.getNotification())
- || hasStyle(statusBarNotification.getNotification(),
- Notification.MessagingStyle.class)) {
+ || hasHighPriorityCharacteristics(
+ mTmpRanking.getChannel(), statusBarNotification)) {
return true;
}
if (mGroupManager.isSummaryOfGroup(statusBarNotification)) {
@@ -257,6 +258,25 @@
return false;
}
+ private boolean hasHighPriorityCharacteristics(NotificationChannel channel,
+ StatusBarNotification statusBarNotification) {
+
+ if (isImportantOngoing(statusBarNotification.getNotification())
+ || statusBarNotification.getNotification().hasMediaSession()
+ || hasPerson(statusBarNotification.getNotification())
+ || hasStyle(statusBarNotification.getNotification(),
+ Notification.MessagingStyle.class)) {
+ // Users who have long pressed and demoted to silent should not see the notification
+ // in the top section
+ if (channel != null && channel.hasUserSetImportance()) {
+ return false;
+ }
+ return true;
+ }
+
+ return false;
+ }
+
private boolean isImportantOngoing(Notification notification) {
return notification.isForegroundService()
&& mTmpRanking.getImportance() >= NotificationManager.IMPORTANCE_LOW;
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/notification/row/NotificationMenuRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java
index ecbb216..d911e1a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java
@@ -16,6 +16,8 @@
package com.android.systemui.statusbar.notification.row;
+import static android.provider.Settings.Secure.SHOW_NOTIFICATION_SNOOZE;
+
import static com.android.systemui.SwipeHelper.SWIPED_FAR_ENOUGH_SIZE_FRACTION;
import android.animation.Animator;
@@ -29,6 +31,7 @@
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.Looper;
+import android.provider.Settings;
import android.service.notification.StatusBarNotification;
import android.util.ArrayMap;
import android.view.LayoutInflater;
@@ -255,9 +258,13 @@
mVertSpaceForIcons = res.getDimensionPixelSize(R.dimen.notification_min_height);
mLeftMenuItems.clear();
mRightMenuItems.clear();
+
+ boolean showSnooze = Settings.Secure.getInt(mContext.getContentResolver(),
+ SHOW_NOTIFICATION_SNOOZE, 0) == 1;
+
// Construct the menu items based on the notification
- if (!isForeground) {
- // Only show snooze for non-foreground notifications
+ if (!isForeground && showSnooze) {
+ // Only show snooze for non-foreground notifications, and if the setting is on
mSnoozeItem = createSnoozeItem(mContext);
}
mAppOpsItem = createAppOpsItem(mContext);
@@ -268,7 +275,7 @@
}
if (!mIsUsingBidirectionalSwipe) {
- if (!isForeground) {
+ if (!isForeground && showSnooze) {
mRightMenuItems.add(mSnoozeItem);
}
mRightMenuItems.add(mInfoItem);
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/NavigationBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarFragment.java
index 337c6b1..d94a335 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarFragment.java
@@ -86,6 +86,7 @@
import com.android.internal.util.LatencyTracker;
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;
import com.android.systemui.fragments.FragmentHostManager;
@@ -170,6 +171,7 @@
public int mDisplayId;
private boolean mIsOnDefaultDisplay;
public boolean mHomeBlockedThisTouch;
+ private ScreenDecorations mScreenDecorations;
private Handler mHandler = Dependency.get(Dependency.MAIN_HANDLER);
@@ -348,12 +350,17 @@
mDisabledFlags2 |= StatusBarManager.DISABLE2_ROTATE_SUGGESTIONS;
}
setDisabled2Flags(mDisabledFlags2);
+
+ mScreenDecorations = SysUiServiceProvider.getComponent(getContext(),
+ ScreenDecorations.class);
+ getBarTransitions().addDarkIntensityListener(mScreenDecorations);
}
@Override
public void onDestroyView() {
super.onDestroyView();
if (mNavigationBarView != null) {
+ mNavigationBarView.getBarTransitions().removeDarkIntensityListener(mScreenDecorations);
mNavigationBarView.getBarTransitions().destroy();
mNavigationBarView.getLightTransitionsController().destroy(getContext());
}
@@ -1020,7 +1027,7 @@
getBarTransitions().transitionTo(barMode, animate);
}
- private BarTransitions getBarTransitions() {
+ public NavigationBarTransitions getBarTransitions() {
return mNavigationBarView.getBarTransitions();
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarTransitions.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarTransitions.java
index 8a28c6f..2b5a28e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarTransitions.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarTransitions.java
@@ -36,9 +36,23 @@
import com.android.systemui.Dependency;
import com.android.systemui.R;
+import java.util.ArrayList;
+import java.util.List;
+
public final class NavigationBarTransitions extends BarTransitions implements
LightBarTransitionsController.DarkIntensityApplier {
+ /**
+ * Notified when the color of nav bar elements changes.
+ */
+ public interface DarkIntensityListener {
+ /**
+ * Called when the color of nav bar elements changes.
+ * @param darkIntensity 0 is the lightest color, 1 is the darkest.
+ */
+ void onDarkIntensity(float darkIntensity);
+ }
+
private final NavigationBarView mView;
private final IStatusBarService mBarService;
private final LightBarTransitionsController mLightTransitionsController;
@@ -49,6 +63,7 @@
private boolean mAutoDim;
private View mNavButtons;
private int mNavBarMode = NAV_BAR_MODE_3BUTTON;
+ private List<DarkIntensityListener> mDarkIntensityListeners;
private final Handler mHandler = Handler.getMain();
private final IWallpaperVisibilityListener mWallpaperVisibilityListener =
@@ -69,6 +84,7 @@
mLightTransitionsController = new LightBarTransitionsController(view.getContext(), this);
mAllowAutoDimWallpaperNotVisible = view.getContext().getResources()
.getBoolean(R.bool.config_navigation_bar_enable_auto_dim_no_visible_wallpaper);
+ mDarkIntensityListeners = new ArrayList();
IWindowManager windowManagerService = Dependency.get(IWindowManager.class);
try {
@@ -168,12 +184,16 @@
applyDarkIntensity(mLightTransitionsController.getCurrentDarkIntensity());
}
+ @Override
public void applyDarkIntensity(float darkIntensity) {
SparseArray<ButtonDispatcher> buttonDispatchers = mView.getButtonDispatchers();
for (int i = buttonDispatchers.size() - 1; i >= 0; i--) {
buttonDispatchers.valueAt(i).setDarkIntensity(darkIntensity);
}
mView.getRotationButtonController().setDarkIntensity(darkIntensity);
+ for (DarkIntensityListener listener : mDarkIntensityListeners) {
+ listener.onDarkIntensity(darkIntensity);
+ }
if (mAutoDim) {
applyLightsOut(false, true);
}
@@ -190,4 +210,18 @@
public void onNavigationModeChanged(int mode) {
mNavBarMode = mode;
}
+
+ /**
+ * Register {@code listener} to be notified when the color of nav bar elements changes.
+ */
+ public void addDarkIntensityListener(DarkIntensityListener listener) {
+ mDarkIntensityListeners.add(listener);
+ }
+
+ /**
+ * Remove {@code listener} from being notified when the color of nav bar elements changes.
+ */
+ public void removeDarkIntensityListener(DarkIntensityListener listener) {
+ mDarkIntensityListeners.remove(listener);
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
index 7682e8a..8a895e1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
@@ -299,7 +299,7 @@
return mTintController;
}
- public BarTransitions getBarTransitions() {
+ public NavigationBarTransitions getBarTransitions() {
return mBarTransitions;
}
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/assist/AssistHandleBehaviorControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/assist/AssistHandleBehaviorControllerTest.java
index 13c92b6..18f114a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/assist/AssistHandleBehaviorControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/assist/AssistHandleBehaviorControllerTest.java
@@ -18,21 +18,27 @@
import static org.mockito.AdditionalAnswers.answerVoid;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+import android.content.ComponentName;
import android.os.Handler;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper.RunWithLooper;
import androidx.test.filters.SmallTest;
+import com.android.internal.app.AssistUtils;
import com.android.systemui.ScreenDecorations;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.recents.OverviewProxyService;
import org.junit.Before;
import org.junit.Test;
@@ -46,29 +52,35 @@
@RunWithLooper
public class AssistHandleBehaviorControllerTest extends SysuiTestCase {
- private final AssistHandleBehavior mTestBehavior = AssistHandleBehavior.TEST;
+ private static final ComponentName COMPONENT_NAME = new ComponentName("", "");
private AssistHandleBehaviorController mAssistHandleBehaviorController;
@Mock private ScreenDecorations mMockScreenDecorations;
+ @Mock private AssistUtils mMockAssistUtils;
@Mock private Handler mMockHandler;
@Mock private AssistHandleBehaviorController.BehaviorController mMockBehaviorController;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
+ mDependency.injectMockDependency(StatusBarStateController.class);
+ mDependency.injectMockDependency(OverviewProxyService.class);
doAnswer(answerVoid(Runnable::run)).when(mMockHandler).post(any(Runnable.class));
doAnswer(answerVoid(Runnable::run)).when(mMockHandler)
.postDelayed(any(Runnable.class), anyLong());
- mTestBehavior.setTestController(mMockBehaviorController);
mAssistHandleBehaviorController =
new AssistHandleBehaviorController(
- mContext, mMockHandler, () -> mMockScreenDecorations);
+ mContext,
+ mMockAssistUtils,
+ mMockHandler, () -> mMockScreenDecorations,
+ mMockBehaviorController);
}
@Test
public void hide_hidesHandlesWhenShowing() {
// Arrange
+ when(mMockAssistUtils.getAssistComponentForUser(anyInt())).thenReturn(COMPONENT_NAME);
mAssistHandleBehaviorController.showAndStay();
reset(mMockScreenDecorations);
@@ -83,6 +95,7 @@
@Test
public void hide_doesNothingWhenHiding() {
// Arrange
+ when(mMockAssistUtils.getAssistComponentForUser(anyInt())).thenReturn(COMPONENT_NAME);
mAssistHandleBehaviorController.hide();
reset(mMockScreenDecorations);
@@ -96,6 +109,7 @@
@Test
public void showAndStay_showsHandlesWhenHiding() {
// Arrange
+ when(mMockAssistUtils.getAssistComponentForUser(anyInt())).thenReturn(COMPONENT_NAME);
mAssistHandleBehaviorController.hide();
reset(mMockScreenDecorations);
@@ -110,6 +124,7 @@
@Test
public void showAndStay_doesNothingWhenShowing() {
// Arrange
+ when(mMockAssistUtils.getAssistComponentForUser(anyInt())).thenReturn(COMPONENT_NAME);
mAssistHandleBehaviorController.showAndStay();
reset(mMockScreenDecorations);
@@ -121,8 +136,23 @@
}
@Test
+ public void showAndStay_doesNothingWhenThereIsNoAssistant() {
+ // Arrange
+ when(mMockAssistUtils.getAssistComponentForUser(anyInt())).thenReturn(null);
+ mAssistHandleBehaviorController.hide();
+ reset(mMockScreenDecorations);
+
+ // Act
+ mAssistHandleBehaviorController.showAndStay();
+
+ // Assert
+ verifyNoMoreInteractions(mMockScreenDecorations);
+ }
+
+ @Test
public void showAndGo_showsThenHidesHandlesWhenHiding() {
// Arrange
+ when(mMockAssistUtils.getAssistComponentForUser(anyInt())).thenReturn(COMPONENT_NAME);
mAssistHandleBehaviorController.hide();
reset(mMockScreenDecorations);
@@ -139,6 +169,7 @@
@Test
public void showAndGo_hidesHandlesAfterTimeoutWhenShowing() {
// Arrange
+ when(mMockAssistUtils.getAssistComponentForUser(anyInt())).thenReturn(COMPONENT_NAME);
mAssistHandleBehaviorController.showAndStay();
reset(mMockScreenDecorations);
@@ -153,6 +184,7 @@
@Test
public void showAndGo_doesNothingIfRecentlyHidden() {
// Arrange
+ when(mMockAssistUtils.getAssistComponentForUser(anyInt())).thenReturn(COMPONENT_NAME);
mAssistHandleBehaviorController.showAndGo();
reset(mMockScreenDecorations);
@@ -164,12 +196,27 @@
}
@Test
+ public void showAndGo_doesNothingWhenThereIsNoAssistant() {
+ // Arrange
+ when(mMockAssistUtils.getAssistComponentForUser(anyInt())).thenReturn(null);
+ mAssistHandleBehaviorController.hide();
+ reset(mMockScreenDecorations);
+
+ // Act
+ mAssistHandleBehaviorController.showAndGo();
+
+ // Assert
+ verifyNoMoreInteractions(mMockScreenDecorations);
+ }
+
+ @Test
public void setBehavior_activatesTheBehaviorWhenInGesturalMode() {
// Arrange
+ when(mMockAssistUtils.getAssistComponentForUser(anyInt())).thenReturn(COMPONENT_NAME);
mAssistHandleBehaviorController.setInGesturalModeForTest(true);
// Act
- mAssistHandleBehaviorController.setBehavior(mTestBehavior);
+ mAssistHandleBehaviorController.setBehavior(AssistHandleBehavior.TEST);
// Assert
verify(mMockBehaviorController).onModeActivated(mContext, mAssistHandleBehaviorController);
@@ -179,8 +226,10 @@
@Test
public void setBehavior_deactivatesThePreviousBehaviorWhenInGesturalMode() {
// Arrange
- mAssistHandleBehaviorController.setBehavior(mTestBehavior);
+ when(mMockAssistUtils.getAssistComponentForUser(anyInt())).thenReturn(COMPONENT_NAME);
+ mAssistHandleBehaviorController.setBehavior(AssistHandleBehavior.TEST);
mAssistHandleBehaviorController.setInGesturalModeForTest(true);
+ reset(mMockBehaviorController);
// Act
mAssistHandleBehaviorController.setBehavior(AssistHandleBehavior.OFF);
@@ -193,10 +242,11 @@
@Test
public void setBehavior_doesNothingWhenNotInGesturalMode() {
// Arrange
+ when(mMockAssistUtils.getAssistComponentForUser(anyInt())).thenReturn(COMPONENT_NAME);
mAssistHandleBehaviorController.setInGesturalModeForTest(false);
// Act
- mAssistHandleBehaviorController.setBehavior(mTestBehavior);
+ mAssistHandleBehaviorController.setBehavior(AssistHandleBehavior.TEST);
// Assert
verifyNoMoreInteractions(mMockBehaviorController);
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/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationDataTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationDataTest.java
index 79a6ac4..6e0ddbf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationDataTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationDataTest.java
@@ -28,6 +28,7 @@
import static com.android.systemui.statusbar.notification.collection.NotificationDataTest.TestableNotificationData.OVERRIDE_CHANNEL;
import static com.android.systemui.statusbar.notification.collection.NotificationDataTest.TestableNotificationData.OVERRIDE_IMPORTANCE;
+import static com.android.systemui.statusbar.notification.collection.NotificationDataTest.TestableNotificationData.OVERRIDE_RANK;
import static com.android.systemui.statusbar.notification.collection.NotificationDataTest.TestableNotificationData.OVERRIDE_VIS_EFFECTS;
import static junit.framework.Assert.assertEquals;
@@ -61,16 +62,12 @@
import android.testing.TestableLooper.RunWithLooper;
import android.util.ArraySet;
-import androidx.test.filters.SmallTest;
-
import com.android.systemui.Dependency;
import com.android.systemui.ForegroundServiceController;
import com.android.systemui.InitController;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.statusbar.NotificationTestHelper;
-import com.android.systemui.statusbar.notification.collection.NotificationData;
import com.android.systemui.statusbar.notification.collection.NotificationData.KeyguardEnvironment;
-import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.phone.NotificationGroupManager;
import com.android.systemui.statusbar.phone.ShadeController;
@@ -83,7 +80,11 @@
import java.util.ArrayList;
import java.util.Collections;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
+
+import androidx.test.filters.SmallTest;
@SmallTest
@RunWith(AndroidTestingRunner.class)
@@ -137,7 +138,9 @@
@Test
public void testChannelSetWhenAdded() {
- mNotificationData.rankingOverrides.putParcelable(OVERRIDE_CHANNEL, NOTIFICATION_CHANNEL);
+ Bundle override = new Bundle();
+ override.putParcelable(OVERRIDE_CHANNEL, NOTIFICATION_CHANNEL);
+ mNotificationData.rankingOverrides.put(mRow.getEntry().key, override);
mNotificationData.add(mRow.getEntry());
assertEquals(NOTIFICATION_CHANNEL, mRow.getEntry().channel);
}
@@ -229,7 +232,9 @@
n.flags = Notification.FLAG_FOREGROUND_SERVICE;
NotificationEntry entry = new NotificationEntry(mMockStatusBarNotification);
mNotificationData.add(entry);
- mNotificationData.rankingOverrides.putInt(OVERRIDE_VIS_EFFECTS, 255);
+ Bundle override = new Bundle();
+ override.putInt(OVERRIDE_VIS_EFFECTS, 255);
+ mNotificationData.rankingOverrides.put(entry.key, override);
assertTrue(entry.isExemptFromDndVisualSuppression());
assertFalse(entry.shouldSuppressAmbient());
@@ -245,7 +250,9 @@
when(mMockStatusBarNotification.getNotification()).thenReturn(n);
NotificationEntry entry = new NotificationEntry(mMockStatusBarNotification);
mNotificationData.add(entry);
- mNotificationData.rankingOverrides.putInt(OVERRIDE_VIS_EFFECTS, 255);
+ Bundle override = new Bundle();
+ override.putInt(OVERRIDE_VIS_EFFECTS, 255);
+ mNotificationData.rankingOverrides.put(entry.key, override);
assertTrue(entry.isExemptFromDndVisualSuppression());
assertFalse(entry.shouldSuppressAmbient());
@@ -257,7 +264,9 @@
NotificationEntry entry = new NotificationEntry(mMockStatusBarNotification);
entry.mIsSystemNotification = true;
mNotificationData.add(entry);
- mNotificationData.rankingOverrides.putInt(OVERRIDE_VIS_EFFECTS, 255);
+ Bundle override = new Bundle();
+ override.putInt(OVERRIDE_VIS_EFFECTS, 255);
+ mNotificationData.rankingOverrides.put(entry.key, override);
assertTrue(entry.isExemptFromDndVisualSuppression());
assertFalse(entry.shouldSuppressAmbient());
@@ -268,8 +277,9 @@
initStatusBarNotification(false);
NotificationEntry entry = new NotificationEntry(mMockStatusBarNotification);
entry.mIsSystemNotification = true;
- mNotificationData.rankingOverrides.putInt(OVERRIDE_VIS_EFFECTS,
- NotificationManager.Policy.SUPPRESSED_EFFECT_AMBIENT);
+ Bundle override = new Bundle();
+ override.putInt(OVERRIDE_VIS_EFFECTS, NotificationManager.Policy.SUPPRESSED_EFFECT_AMBIENT);
+ mNotificationData.rankingOverrides.put(entry.key, override);
mNotificationData.add(entry);
when(mMockStatusBarNotification.getNotification()).thenReturn(
@@ -395,11 +405,13 @@
Notification notification = mock(Notification.class);
when(notification.isForegroundService()).thenReturn(true);
- mNotificationData.rankingOverrides.putInt(OVERRIDE_IMPORTANCE, IMPORTANCE_MIN);
-
StatusBarNotification sbn = new StatusBarNotification("pkg", "pkg", 0, "tag", 0, 0,
notification, mContext.getUser(), "", 0);
+ Bundle override = new Bundle();
+ override.putInt(OVERRIDE_IMPORTANCE, IMPORTANCE_MIN);
+ mNotificationData.rankingOverrides.put(sbn.getKey(), override);
+
assertFalse(mNotificationData.isHighPriority(sbn));
}
@@ -408,12 +420,112 @@
Notification notification = mock(Notification.class);
when(notification.isForegroundService()).thenReturn(true);
- mNotificationData.rankingOverrides.putInt(OVERRIDE_IMPORTANCE, IMPORTANCE_LOW);
+ StatusBarNotification sbn = new StatusBarNotification("pkg", "pkg", 0, "tag", 0, 0,
+ notification, mContext.getUser(), "", 0);
+
+ Bundle override = new Bundle();
+ override.putInt(OVERRIDE_IMPORTANCE, IMPORTANCE_LOW);
+ mNotificationData.rankingOverrides.put(sbn.getKey(), override);
+
+ assertTrue(mNotificationData.isHighPriority(sbn));
+ }
+
+ @Test
+ public void userChangeTrumpsHighPriorityCharacteristics() {
+ Person person = new Person.Builder()
+ .setName("name")
+ .setKey("abc")
+ .setUri("uri")
+ .setBot(true)
+ .build();
+
+ Notification notification = new Notification.Builder(mContext, "test")
+ .addPerson(person)
+ .setStyle(new Notification.MessagingStyle(""))
+ .setFlag(Notification.FLAG_FOREGROUND_SERVICE, true)
+ .build();
StatusBarNotification sbn = new StatusBarNotification("pkg", "pkg", 0, "tag", 0, 0,
notification, mContext.getUser(), "", 0);
- assertTrue(mNotificationData.isHighPriority(sbn));
+ NotificationChannel channel = new NotificationChannel("a", "a", IMPORTANCE_LOW);
+ channel.lockFields(NotificationChannel.USER_LOCKED_IMPORTANCE);
+
+ Bundle override = new Bundle();
+ override.putParcelable(OVERRIDE_CHANNEL, channel);
+ mNotificationData.rankingOverrides.put(sbn.getKey(), override);
+
+ assertFalse(mNotificationData.isHighPriority(sbn));
+ }
+
+ @Test
+ public void testSort_highPriorityTrumpsNMSRank() {
+ // NMS rank says A and then B. But A is not high priority and B is, so B should sort in
+ // front
+ Notification aN = new Notification.Builder(mContext, "test")
+ .setStyle(new Notification.MessagingStyle(""))
+ .build();
+ StatusBarNotification aSbn = new StatusBarNotification("pkg", "pkg", 0, "tag", 0, 0,
+ aN, mContext.getUser(), "", 0);
+ NotificationEntry a = new NotificationEntry(aSbn);
+ a.setRow(mock(ExpandableNotificationRow.class));
+ a.setIsHighPriority(false);
+
+ Bundle override = new Bundle();
+ override.putInt(OVERRIDE_IMPORTANCE, IMPORTANCE_LOW);
+ override.putInt(OVERRIDE_RANK, 1);
+ mNotificationData.rankingOverrides.put(a.key, override);
+
+ Notification bN = new Notification.Builder(mContext, "test")
+ .setStyle(new Notification.MessagingStyle(""))
+ .build();
+ StatusBarNotification bSbn = new StatusBarNotification("pkg2", "pkg2", 0, "tag", 0, 0,
+ bN, mContext.getUser(), "", 0);
+ NotificationEntry b = new NotificationEntry(bSbn);
+ b.setIsHighPriority(true);
+ b.setRow(mock(ExpandableNotificationRow.class));
+
+ Bundle bOverride = new Bundle();
+ bOverride.putInt(OVERRIDE_IMPORTANCE, IMPORTANCE_LOW);
+ bOverride.putInt(OVERRIDE_RANK, 2);
+ mNotificationData.rankingOverrides.put(b.key, bOverride);
+
+ assertEquals(1, mNotificationData.mRankingComparator.compare(a, b));
+ }
+
+ @Test
+ public void testSort_samePriorityUsesNMSRank() {
+ // NMS rank says A and then B. But A is not high priority and B is, so B should sort in
+ // front
+ Notification aN = new Notification.Builder(mContext, "test")
+ .setStyle(new Notification.MessagingStyle(""))
+ .build();
+ StatusBarNotification aSbn = new StatusBarNotification("pkg", "pkg", 0, "tag", 0, 0,
+ aN, mContext.getUser(), "", 0);
+ NotificationEntry a = new NotificationEntry(aSbn);
+ a.setRow(mock(ExpandableNotificationRow.class));
+ a.setIsHighPriority(false);
+
+ Bundle override = new Bundle();
+ override.putInt(OVERRIDE_IMPORTANCE, IMPORTANCE_LOW);
+ override.putInt(OVERRIDE_RANK, 1);
+ mNotificationData.rankingOverrides.put(a.key, override);
+
+ Notification bN = new Notification.Builder(mContext, "test")
+ .setStyle(new Notification.MessagingStyle(""))
+ .build();
+ StatusBarNotification bSbn = new StatusBarNotification("pkg2", "pkg2", 0, "tag", 0, 0,
+ bN, mContext.getUser(), "", 0);
+ NotificationEntry b = new NotificationEntry(bSbn);
+ b.setRow(mock(ExpandableNotificationRow.class));
+ b.setIsHighPriority(false);
+
+ Bundle bOverride = new Bundle();
+ bOverride.putInt(OVERRIDE_IMPORTANCE, IMPORTANCE_LOW);
+ bOverride.putInt(OVERRIDE_RANK, 2);
+ mNotificationData.rankingOverrides.put(b.key, bOverride);
+
+ assertEquals(-1, mNotificationData.mRankingComparator.compare(a, b));
}
private void initStatusBarNotification(boolean allowDuringSetup) {
@@ -449,7 +561,7 @@
public static final String OVERRIDE_SMART_REPLIES = "sr";
public static final String OVERRIDE_BUBBLE = "cb";
- public Bundle rankingOverrides = new Bundle();
+ public Map<String, Bundle> rankingOverrides = new HashMap<>();
@Override
protected boolean getRanking(String key, Ranking outRanking) {
@@ -475,40 +587,41 @@
currentReplies.addAll(outRanking.getSmartReplies());
}
- outRanking.populate(key,
- rankingOverrides.getInt(OVERRIDE_RANK, outRanking.getRank()),
- rankingOverrides.getBoolean(OVERRIDE_DND,
- outRanking.matchesInterruptionFilter()),
- rankingOverrides.getInt(OVERRIDE_VIS_OVERRIDE,
- outRanking.getVisibilityOverride()),
- rankingOverrides.getInt(OVERRIDE_VIS_EFFECTS,
- outRanking.getSuppressedVisualEffects()),
- rankingOverrides.getInt(OVERRIDE_IMPORTANCE, outRanking.getImportance()),
- rankingOverrides.getCharSequence(OVERRIDE_IMP_EXP,
- outRanking.getImportanceExplanation()),
- rankingOverrides.getString(OVERRIDE_GROUP, outRanking.getOverrideGroupKey()),
- rankingOverrides.containsKey(OVERRIDE_CHANNEL)
- ? (NotificationChannel) rankingOverrides.getParcelable(OVERRIDE_CHANNEL)
- : outRanking.getChannel(),
- rankingOverrides.containsKey(OVERRIDE_PEOPLE)
- ? rankingOverrides.getStringArrayList(OVERRIDE_PEOPLE)
- : currentAdditionalPeople,
- rankingOverrides.containsKey(OVERRIDE_SNOOZE_CRITERIA)
- ? rankingOverrides.getParcelableArrayList(OVERRIDE_SNOOZE_CRITERIA)
- : currentSnooze,
- rankingOverrides.getBoolean(OVERRIDE_BADGE, outRanking.canShowBadge()),
- rankingOverrides.getInt(OVERRIDE_USER_SENTIMENT, outRanking.getUserSentiment()),
- rankingOverrides.getBoolean(OVERRIDE_HIDDEN, outRanking.isSuspended()),
- rankingOverrides.getLong(OVERRIDE_LAST_ALERTED,
- outRanking.getLastAudiblyAlertedMillis()),
- rankingOverrides.getBoolean(OVERRIDE_NOISY, outRanking.isNoisy()),
- rankingOverrides.containsKey(OVERRIDE_SMART_ACTIONS)
- ? rankingOverrides.getParcelableArrayList(OVERRIDE_SMART_ACTIONS)
- : currentActions,
- rankingOverrides.containsKey(OVERRIDE_SMART_REPLIES)
- ? rankingOverrides.getCharSequenceArrayList(OVERRIDE_SMART_REPLIES)
- : currentReplies,
- rankingOverrides.getBoolean(OVERRIDE_BUBBLE, outRanking.canBubble()));
+ if (rankingOverrides.get(key) != null) {
+ Bundle overrides = rankingOverrides.get(key);
+ outRanking.populate(key,
+ overrides.getInt(OVERRIDE_RANK, outRanking.getRank()),
+ overrides.getBoolean(OVERRIDE_DND, outRanking.matchesInterruptionFilter()),
+ overrides.getInt(OVERRIDE_VIS_OVERRIDE, outRanking.getVisibilityOverride()),
+ overrides.getInt(OVERRIDE_VIS_EFFECTS,
+ outRanking.getSuppressedVisualEffects()),
+ overrides.getInt(OVERRIDE_IMPORTANCE, outRanking.getImportance()),
+ overrides.getCharSequence(OVERRIDE_IMP_EXP,
+ outRanking.getImportanceExplanation()),
+ overrides.getString(OVERRIDE_GROUP, outRanking.getOverrideGroupKey()),
+ overrides.containsKey(OVERRIDE_CHANNEL)
+ ? (NotificationChannel) overrides.getParcelable(OVERRIDE_CHANNEL)
+ : outRanking.getChannel(),
+ overrides.containsKey(OVERRIDE_PEOPLE)
+ ? overrides.getStringArrayList(OVERRIDE_PEOPLE)
+ : currentAdditionalPeople,
+ overrides.containsKey(OVERRIDE_SNOOZE_CRITERIA)
+ ? overrides.getParcelableArrayList(OVERRIDE_SNOOZE_CRITERIA)
+ : currentSnooze,
+ overrides.getBoolean(OVERRIDE_BADGE, outRanking.canShowBadge()),
+ overrides.getInt(OVERRIDE_USER_SENTIMENT, outRanking.getUserSentiment()),
+ overrides.getBoolean(OVERRIDE_HIDDEN, outRanking.isSuspended()),
+ overrides.getLong(OVERRIDE_LAST_ALERTED,
+ outRanking.getLastAudiblyAlertedMillis()),
+ overrides.getBoolean(OVERRIDE_NOISY, outRanking.isNoisy()),
+ overrides.containsKey(OVERRIDE_SMART_ACTIONS)
+ ? overrides.getParcelableArrayList(OVERRIDE_SMART_ACTIONS)
+ : currentActions,
+ overrides.containsKey(OVERRIDE_SMART_REPLIES)
+ ? overrides.getCharSequenceArrayList(OVERRIDE_SMART_REPLIES)
+ : currentReplies,
+ overrides.getBoolean(OVERRIDE_BUBBLE, outRanking.canBubble()));
+ }
return true;
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationMenuRowTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationMenuRowTest.java
index 43ea92f..13b9d56 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationMenuRowTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationMenuRowTest.java
@@ -15,6 +15,7 @@
package com.android.systemui.statusbar.notification.row;
import static android.provider.Settings.Secure.NOTIFICATION_NEW_INTERRUPTION_MODEL;
+import static android.provider.Settings.Secure.SHOW_NOTIFICATION_SNOOZE;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertFalse;
@@ -100,8 +101,31 @@
@Test
public void testNoAppOpsInSlowSwipe() {
- Settings.Secure.putInt(mContext.getContentResolver(),
- NOTIFICATION_NEW_INTERRUPTION_MODEL, 0);
+ Settings.Secure.putInt(mContext.getContentResolver(), SHOW_NOTIFICATION_SNOOZE, 0);
+
+ NotificationMenuRow row = new NotificationMenuRow(mContext);
+ row.createMenu(mRow, null);
+
+ ViewGroup container = (ViewGroup) row.getMenuView();
+ // noti blocking
+ assertEquals(1, container.getChildCount());
+ }
+
+ @Test
+ public void testNoSnoozeInSlowSwipe() {
+ Settings.Secure.putInt(mContext.getContentResolver(), SHOW_NOTIFICATION_SNOOZE, 0);
+
+ NotificationMenuRow row = new NotificationMenuRow(mContext);
+ row.createMenu(mRow, null);
+
+ ViewGroup container = (ViewGroup) row.getMenuView();
+ // just for noti blocking
+ assertEquals(1, container.getChildCount());
+ }
+
+ @Test
+ public void testSnoozeInSlowSwipe() {
+ Settings.Secure.putInt(mContext.getContentResolver(), SHOW_NOTIFICATION_SNOOZE, 1);
NotificationMenuRow row = new NotificationMenuRow(mContext);
row.createMenu(mRow, null);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NavigationBarFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NavigationBarFragmentTest.java
index 4181d38..faf5a97 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NavigationBarFragmentTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NavigationBarFragmentTest.java
@@ -268,7 +268,7 @@
when(view.getRecentsButton()).thenReturn(mock(ButtonDispatcher.class));
when(view.getAccessibilityButton()).thenReturn(mock(ButtonDispatcher.class));
when(view.getRotateSuggestionButton()).thenReturn(mock(RotationContextButton.class));
- when(view.getBarTransitions()).thenReturn(mock(BarTransitions.class));
+ when(view.getBarTransitions()).thenReturn(mock(NavigationBarTransitions.class));
when(view.getLightTransitionsController()).thenReturn(
mock(LightBarTransitionsController.class));
when(view.getRotationButtonController()).thenReturn(
diff --git a/packages/overlays/NavigationBarModeGesturalOverlay/res/values/dimens.xml b/packages/overlays/NavigationBarModeGesturalOverlay/res/values/dimens.xml
index 987d203..1232201 100644
--- a/packages/overlays/NavigationBarModeGesturalOverlay/res/values/dimens.xml
+++ b/packages/overlays/NavigationBarModeGesturalOverlay/res/values/dimens.xml
@@ -25,4 +25,6 @@
<dimen name="navigation_bar_width">16dp</dimen>
<!-- Height of the bottom navigation / system bar. -->
<dimen name="navigation_bar_frame_height">48dp</dimen>
+ <!-- The height of the bottom navigation gesture area. -->
+ <dimen name="navigation_bar_gesture_height">32dp</dimen>
</resources>
\ No newline at end of file
diff --git a/services/appprediction/java/com/android/server/appprediction/AppPredictionPerUserService.java b/services/appprediction/java/com/android/server/appprediction/AppPredictionPerUserService.java
index 430abf5..10ba9a5 100644
--- a/services/appprediction/java/com/android/server/appprediction/AppPredictionPerUserService.java
+++ b/services/appprediction/java/com/android/server/appprediction/AppPredictionPerUserService.java
@@ -28,13 +28,17 @@
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ParceledListSlice;
import android.content.pm.ServiceInfo;
+import android.os.IBinder;
import android.os.RemoteException;
import android.service.appprediction.AppPredictionService;
+import android.util.ArrayMap;
import android.util.Slog;
import com.android.internal.annotations.GuardedBy;
import com.android.server.infra.AbstractPerUserSystemService;
+import java.util.ArrayList;
+
/**
* Per-user instance of {@link AppPredictionManagerService}.
*/
@@ -48,6 +52,17 @@
@GuardedBy("mLock")
private RemoteAppPredictionService mRemoteService;
+ /**
+ * When {@code true}, remote service died but service state is kept so it's restored after
+ * the system re-binds to it.
+ */
+ @GuardedBy("mLock")
+ private boolean mZombie;
+
+ @GuardedBy("mLock")
+ private final ArrayMap<AppPredictionSessionId, AppPredictionSessionInfo> mSessionInfos =
+ new ArrayMap<>();
+
protected AppPredictionPerUserService(AppPredictionManagerService master,
Object lock, int userId) {
super(master, lock, userId);
@@ -92,6 +107,16 @@
final RemoteAppPredictionService service = getRemoteServiceLocked();
if (service != null) {
service.onCreatePredictionSession(context, sessionId);
+
+ mSessionInfos.put(sessionId, new AppPredictionSessionInfo(sessionId, context, () -> {
+ synchronized (mLock) {
+ AppPredictionSessionInfo sessionInfo = mSessionInfos.get(sessionId);
+ if (sessionInfo != null) {
+ sessionInfo.removeAllCallbacksLocked();
+ mSessionInfos.remove(sessionId);
+ }
+ }
+ }));
}
}
@@ -140,6 +165,11 @@
final RemoteAppPredictionService service = getRemoteServiceLocked();
if (service != null) {
service.registerPredictionUpdates(sessionId, callback);
+
+ AppPredictionSessionInfo sessionInfo = mSessionInfos.get(sessionId);
+ if (sessionInfo != null) {
+ sessionInfo.addCallbackLocked(callback);
+ }
}
}
@@ -152,6 +182,11 @@
final RemoteAppPredictionService service = getRemoteServiceLocked();
if (service != null) {
service.unregisterPredictionUpdates(sessionId, callback);
+
+ AppPredictionSessionInfo sessionInfo = mSessionInfos.get(sessionId);
+ if (sessionInfo != null) {
+ sessionInfo.removeCallbackLocked(callback);
+ }
}
}
@@ -174,6 +209,12 @@
final RemoteAppPredictionService service = getRemoteServiceLocked();
if (service != null) {
service.onDestroyPredictionSession(sessionId);
+
+ AppPredictionSessionInfo sessionInfo = mSessionInfos.get(sessionId);
+ if (sessionInfo != null) {
+ sessionInfo.removeAllCallbacksLocked();
+ mSessionInfos.remove(sessionId);
+ }
}
}
@@ -182,17 +223,54 @@
if (isDebug()) {
Slog.d(TAG, "onFailureOrTimeout(): timed out=" + timedOut);
}
-
// Do nothing, we are just proxying to the prediction service
}
@Override
+ public void onConnectedStateChanged(boolean connected) {
+ if (isDebug()) {
+ Slog.d(TAG, "onConnectedStateChanged(): connected=" + connected);
+ }
+ if (connected) {
+ synchronized (mLock) {
+ if (mZombie) {
+ // Sanity check - shouldn't happen
+ if (mRemoteService == null) {
+ Slog.w(TAG, "Cannot resurrect sessions because remote service is null");
+ return;
+ }
+ mZombie = false;
+ resurrectSessionsLocked();
+ }
+ }
+ }
+ }
+
+ @Override
public void onServiceDied(RemoteAppPredictionService service) {
if (isDebug()) {
- Slog.d(TAG, "onServiceDied():");
+ Slog.w(TAG, "onServiceDied(): service=" + service);
+ }
+ synchronized (mLock) {
+ mZombie = true;
+ }
+ // Do nothing, eventually the system will bind to the remote service again...
+ }
+
+ /**
+ * Called after the remote service connected, it's used to restore state from a 'zombie'
+ * service (i.e., after it died).
+ */
+ private void resurrectSessionsLocked() {
+ final int numSessions = mSessionInfos.size();
+ if (isDebug()) {
+ Slog.d(TAG, "Resurrecting remote service (" + mRemoteService + ") on "
+ + numSessions + " sessions.");
}
- // Do nothing, we are just proxying to the prediction service
+ for (AppPredictionSessionInfo sessionInfo : mSessionInfos.values()) {
+ sessionInfo.resurrectSessionLocked(this);
+ }
}
@GuardedBy("mLock")
@@ -215,4 +293,57 @@
return mRemoteService;
}
+
+ private static final class AppPredictionSessionInfo {
+ private final AppPredictionSessionId mSessionId;
+ private final AppPredictionContext mContext;
+ private final ArrayList<IPredictionCallback> mCallbacks = new ArrayList<>();
+ private final IBinder.DeathRecipient mBinderDeathHandler;
+
+ AppPredictionSessionInfo(AppPredictionSessionId id, AppPredictionContext context,
+ IBinder.DeathRecipient binderDeathHandler) {
+ mSessionId = id;
+ mContext = context;
+ mBinderDeathHandler = binderDeathHandler;
+ }
+
+ void addCallbackLocked(IPredictionCallback callback) {
+ if (mBinderDeathHandler != null) {
+ try {
+ callback.asBinder().linkToDeath(mBinderDeathHandler, 0);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Failed to link to death: " + e);
+ }
+ }
+ mCallbacks.add(callback);
+ }
+
+ void removeCallbackLocked(IPredictionCallback callback) {
+ if (mBinderDeathHandler != null) {
+ callback.asBinder().unlinkToDeath(mBinderDeathHandler, 0);
+ }
+ mCallbacks.remove(callback);
+ }
+
+ void removeAllCallbacksLocked() {
+ if (mBinderDeathHandler != null) {
+ for (IPredictionCallback callback : mCallbacks) {
+ callback.asBinder().unlinkToDeath(mBinderDeathHandler, 0);
+ }
+ }
+ mCallbacks.clear();
+ }
+
+ void resurrectSessionLocked(AppPredictionPerUserService service) {
+ if (service.isDebug()) {
+ Slog.d(TAG, "Resurrecting remote service (" + service.getRemoteServiceLocked()
+ + ") for session Id=" + mSessionId + " and "
+ + mCallbacks.size() + " callbacks.");
+ }
+ service.onCreatePredictionSessionLocked(mContext, mSessionId);
+ for (IPredictionCallback callback : mCallbacks) {
+ service.registerPredictionUpdatesLocked(mSessionId, callback);
+ }
+ }
+ }
}
diff --git a/services/appprediction/java/com/android/server/appprediction/RemoteAppPredictionService.java b/services/appprediction/java/com/android/server/appprediction/RemoteAppPredictionService.java
index 19226be..c82e7a0 100644
--- a/services/appprediction/java/com/android/server/appprediction/RemoteAppPredictionService.java
+++ b/services/appprediction/java/com/android/server/appprediction/RemoteAppPredictionService.java
@@ -42,6 +42,8 @@
private static final long TIMEOUT_REMOTE_REQUEST_MILLIS = 2 * DateUtils.SECOND_IN_MILLIS;
+ private final RemoteAppPredictionServiceCallbacks mCallback;
+
public RemoteAppPredictionService(Context context, String serviceInterface,
ComponentName componentName, int userId,
RemoteAppPredictionServiceCallbacks callback, boolean bindInstantServiceAllowed,
@@ -50,6 +52,7 @@
context.getMainThreadHandler(),
bindInstantServiceAllowed ? Context.BIND_ALLOW_INSTANT : 0,
verbose, /* initialCapacity= */ 1);
+ mCallback = callback;
}
@Override
@@ -141,5 +144,17 @@
* Notifies a the failure or timeout of a remote call.
*/
void onFailureOrTimeout(boolean timedOut);
+
+ /**
+ * Notifies change in connected state of the remote service.
+ */
+ void onConnectedStateChanged(boolean connected);
+ }
+
+ @Override // from AbstractRemoteService
+ protected void handleOnConnectedStateChanged(boolean connected) {
+ if (mCallback != null) {
+ mCallback.onConnectedStateChanged(connected);
+ }
}
}
diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java
index 0a1dbff..45f7360 100644
--- a/services/core/java/com/android/server/ConnectivityService.java
+++ b/services/core/java/com/android/server/ConnectivityService.java
@@ -4363,7 +4363,7 @@
/**
* @return VPN information for accounting, or null if we can't retrieve all required
- * information, e.g primary underlying iface.
+ * information, e.g underlying ifaces.
*/
@Nullable
private VpnInfo createVpnInfo(Vpn vpn) {
@@ -4375,17 +4375,24 @@
// see VpnService.setUnderlyingNetworks()'s javadoc about how to interpret
// the underlyingNetworks list.
if (underlyingNetworks == null) {
- NetworkAgentInfo defaultNetwork = getDefaultNetwork();
- if (defaultNetwork != null && defaultNetwork.linkProperties != null) {
- info.primaryUnderlyingIface = getDefaultNetwork().linkProperties.getInterfaceName();
- }
- } else if (underlyingNetworks.length > 0) {
- LinkProperties linkProperties = getLinkProperties(underlyingNetworks[0]);
- if (linkProperties != null) {
- info.primaryUnderlyingIface = linkProperties.getInterfaceName();
+ NetworkAgentInfo defaultNai = getDefaultNetwork();
+ if (defaultNai != null) {
+ underlyingNetworks = new Network[] { defaultNai.network };
}
}
- return info.primaryUnderlyingIface == null ? null : info;
+ if (underlyingNetworks != null && underlyingNetworks.length > 0) {
+ List<String> interfaces = new ArrayList<>();
+ for (Network network : underlyingNetworks) {
+ LinkProperties lp = getLinkProperties(network);
+ if (lp != null && !TextUtils.isEmpty(lp.getInterfaceName())) {
+ interfaces.add(lp.getInterfaceName());
+ }
+ }
+ if (!interfaces.isEmpty()) {
+ info.underlyingIfaces = interfaces.toArray(new String[interfaces.size()]);
+ }
+ }
+ return info.underlyingIfaces == null ? null : info;
}
/**
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/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java
index bbbec66..d2b992b 100644
--- a/services/core/java/com/android/server/StorageManagerService.java
+++ b/services/core/java/com/android/server/StorageManagerService.java
@@ -2842,8 +2842,9 @@
}
}
+ /** Not thread safe */
class AppFuseMountScope extends AppFuseBridge.MountScope {
- boolean opened = false;
+ private boolean mMounted = false;
public AppFuseMountScope(int uid, int mountId) {
super(uid, mountId);
@@ -2852,8 +2853,9 @@
@Override
public ParcelFileDescriptor open() throws NativeDaemonConnectorException {
try {
- return new ParcelFileDescriptor(
- mVold.mountAppFuse(uid, mountId));
+ final FileDescriptor fd = mVold.mountAppFuse(uid, mountId);
+ mMounted = true;
+ return new ParcelFileDescriptor(fd);
} catch (Exception e) {
throw new NativeDaemonConnectorException("Failed to mount", e);
}
@@ -2872,9 +2874,9 @@
@Override
public void close() throws Exception {
- if (opened) {
+ if (mMounted) {
mVold.unmountAppFuse(uid, mountId);
- opened = false;
+ mMounted = false;
}
}
}
@@ -3582,6 +3584,10 @@
}
final String[] packagesForUid = mIPackageManager.getPackagesForUid(uid);
+ if (ArrayUtils.isEmpty(packagesForUid)) {
+ // It's possible the package got uninstalled already, so just ignore.
+ return Zygote.MOUNT_EXTERNAL_NONE;
+ }
if (packageName == null) {
packageName = packagesForUid[0];
}
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index 90266f1..fae853c 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -3250,6 +3250,7 @@
int memFactor = mAm.mProcessStats.getMemFactorLocked();
long now = SystemClock.uptimeMillis();
r.tracker.setExecuting(false, memFactor, now);
+ r.tracker.setForeground(false, memFactor, now);
r.tracker.setBound(false, memFactor, now);
r.tracker.setStarted(false, memFactor, now);
}
@@ -3293,8 +3294,10 @@
}
r.executeFg = false;
if (r.tracker != null) {
- r.tracker.setExecuting(false, mAm.mProcessStats.getMemFactorLocked(),
- SystemClock.uptimeMillis());
+ final int memFactor = mAm.mProcessStats.getMemFactorLocked();
+ final long now = SystemClock.uptimeMillis();
+ r.tracker.setExecuting(false, memFactor, now);
+ r.tracker.setForeground(false, memFactor, now);
if (finishing) {
r.tracker.clearCurrentOwner(r, false);
r.tracker = null;
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/connectivity/NetworkNotificationManager.java b/services/core/java/com/android/server/connectivity/NetworkNotificationManager.java
index 0910dac2..f6735d9 100644
--- a/services/core/java/com/android/server/connectivity/NetworkNotificationManager.java
+++ b/services/core/java/com/android/server/connectivity/NetworkNotificationManager.java
@@ -237,9 +237,15 @@
+ getTransportName(transportType));
return;
}
-
- final String channelId = highPriority ? SystemNotificationChannels.NETWORK_ALERTS :
- SystemNotificationChannels.NETWORK_STATUS;
+ // When replacing an existing notification for a given network, don't alert, just silently
+ // update the existing notification. Note that setOnlyAlertOnce() will only work for the
+ // same id, and the id used here is the NotificationType which is different in every type of
+ // notification. This is required because the notification metrics only track the ID but not
+ // the tag.
+ final boolean hasPreviousNotification = previousNotifyType != null;
+ final String channelId = (highPriority && !hasPreviousNotification)
+ ? SystemNotificationChannels.NETWORK_ALERTS
+ : SystemNotificationChannels.NETWORK_STATUS;
Notification.Builder builder = new Notification.Builder(mContext, channelId)
.setWhen(System.currentTimeMillis())
.setShowWhen(notifyType == NotificationType.NETWORK_SWITCH)
diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java
index 0271d3b..40a4820 100644
--- a/services/core/java/com/android/server/connectivity/Vpn.java
+++ b/services/core/java/com/android/server/connectivity/Vpn.java
@@ -953,30 +953,21 @@
return false;
}
- LinkProperties lp = makeLinkProperties();
- final boolean hadInternetCapability = mNetworkCapabilities.hasCapability(
- NetworkCapabilities.NET_CAPABILITY_INTERNET);
- final boolean willHaveInternetCapability = providesRoutesToMostDestinations(lp);
- if (hadInternetCapability != willHaveInternetCapability) {
- // A seamless handover would have led to a change to INTERNET capability, which
- // is supposed to be immutable for a given network. In this case bail out and do not
- // perform handover.
- Log.i(TAG, "Handover not possible due to changes to INTERNET capability");
- return false;
- }
-
- agent.sendLinkProperties(lp);
+ agent.sendLinkProperties(makeLinkProperties());
return true;
}
private void agentConnect() {
LinkProperties lp = makeLinkProperties();
- if (providesRoutesToMostDestinations(lp)) {
- mNetworkCapabilities.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
- } else {
- mNetworkCapabilities.removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
- }
+ // VPN either provide a default route (IPv4 or IPv6 or both), or they are a split tunnel
+ // that falls back to the default network, which by definition provides INTERNET (unless
+ // there is no default network, in which case none of this matters in any sense).
+ // Also, it guarantees that when a VPN applies to an app, the VPN will always be reported
+ // as the network by getDefaultNetwork and registerDefaultNetworkCallback. This in turn
+ // protects the invariant that apps calling CM#bindProcessToNetwork(getDefaultNetwork())
+ // the same as if they use the default network.
+ mNetworkCapabilities.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
mNetworkInfo.setDetailedState(DetailedState.CONNECTING, null, null);
@@ -1846,10 +1837,11 @@
if (!profile.searchDomains.isEmpty()) {
config.searchDomains = Arrays.asList(profile.searchDomains.split(" +"));
}
- startLegacyVpn(config, racoon, mtpd);
+ startLegacyVpn(config, racoon, mtpd, profile);
}
- private synchronized void startLegacyVpn(VpnConfig config, String[] racoon, String[] mtpd) {
+ private synchronized void startLegacyVpn(VpnConfig config, String[] racoon, String[] mtpd,
+ VpnProfile profile) {
stopLegacyVpnPrivileged();
// Prepare for the new request.
@@ -1857,7 +1849,7 @@
updateState(DetailedState.CONNECTING, "startLegacyVpn");
// Start a new LegacyVpnRunner and we are done!
- mLegacyVpnRunner = new LegacyVpnRunner(config, racoon, mtpd);
+ mLegacyVpnRunner = new LegacyVpnRunner(config, racoon, mtpd, profile);
mLegacyVpnRunner.start();
}
@@ -1923,6 +1915,7 @@
private final String mOuterInterface;
private final AtomicInteger mOuterConnection =
new AtomicInteger(ConnectivityManager.TYPE_NONE);
+ private final VpnProfile mProfile;
private long mBringupStartTime = -1;
@@ -1949,7 +1942,7 @@
}
};
- public LegacyVpnRunner(VpnConfig config, String[] racoon, String[] mtpd) {
+ LegacyVpnRunner(VpnConfig config, String[] racoon, String[] mtpd, VpnProfile profile) {
super(TAG);
mConfig = config;
mDaemons = new String[] {"racoon", "mtpd"};
@@ -1965,6 +1958,8 @@
// registering
mOuterInterface = mConfig.interfaze;
+ mProfile = profile;
+
if (!TextUtils.isEmpty(mOuterInterface)) {
final ConnectivityManager cm = ConnectivityManager.from(mContext);
for (Network network : cm.getAllNetworks()) {
@@ -2177,7 +2172,7 @@
}
// Add a throw route for the VPN server endpoint, if one was specified.
- String endpoint = parameters[5];
+ String endpoint = parameters[5].isEmpty() ? mProfile.server : parameters[5];
if (!endpoint.isEmpty()) {
try {
InetAddress addr = InetAddress.parseNumericAddress(endpoint);
diff --git a/services/core/java/com/android/server/job/controllers/QuotaController.java b/services/core/java/com/android/server/job/controllers/QuotaController.java
index f560d69..ef6944e 100644
--- a/services/core/java/com/android/server/job/controllers/QuotaController.java
+++ b/services/core/java/com/android/server/job/controllers/QuotaController.java
@@ -16,6 +16,10 @@
package com.android.server.job.controllers;
+import static android.text.format.DateUtils.HOUR_IN_MILLIS;
+import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
+import static android.text.format.DateUtils.SECOND_IN_MILLIS;
+
import static com.android.server.job.JobSchedulerService.ACTIVE_INDEX;
import static com.android.server.job.JobSchedulerService.FREQUENT_INDEX;
import static com.android.server.job.JobSchedulerService.NEVER_INDEX;
@@ -75,17 +79,28 @@
/**
* Controller that tracks whether an app has exceeded its standby bucket quota.
*
- * Each job in each bucket is given 10 minutes to run within its respective time window. Active
- * jobs can run indefinitely, working set jobs can run for 10 minutes within a 2 hour window,
- * frequent jobs get to run 10 minutes in an 8 hour window, and rare jobs get to run 10 minutes in
- * a 24 hour window. The windows are rolling, so as soon as a job would have some quota based on its
- * bucket, it will be eligible to run. When a job's bucket changes, its new quota is immediately
- * applied to it.
+ * With initial defaults, each app in each bucket is given 10 minutes to run within its respective
+ * time window. Active jobs can run indefinitely, working set jobs can run for 10 minutes within a
+ * 2 hour window, frequent jobs get to run 10 minutes in an 8 hour window, and rare jobs get to run
+ * 10 minutes in a 24 hour window. The windows are rolling, so as soon as a job would have some
+ * quota based on its bucket, it will be eligible to run. When a job's bucket changes, its new
+ * quota is immediately applied to it.
+ *
+ * Job and session count limits are included to prevent abuse/spam. Each bucket has its own limit on
+ * the number of jobs or sessions that can run within the window. Regardless of bucket, apps will
+ * not be allowed to run more than 20 jobs within the past 10 minutes.
*
* Jobs are throttled while an app is not in a foreground state. All jobs are allowed to run
* freely when an app enters the foreground state and are restricted when the app leaves the
- * foreground state. However, jobs that are started while the app is in the TOP state are not
- * restricted regardless of the app's state change.
+ * foreground state. However, jobs that are started while the app is in the TOP state do not count
+ * towards any quota and are not restricted regardless of the app's state change.
+ *
+ * Jobs will not be throttled when the device is charging. The device is considered to be charging
+ * once the {@link BatteryManager#ACTION_CHARGING} intent has been broadcast.
+ *
+ * Note: all limits are enforced per bucket window unless explicitly stated otherwise.
+ * All stated values are configurable and subject to change. See {@link QcConstants} for current
+ * defaults.
*
* Test: atest com.android.server.job.controllers.QuotaControllerTest
*/
@@ -94,8 +109,6 @@
private static final boolean DEBUG = JobSchedulerService.DEBUG
|| Log.isLoggable(TAG, Log.DEBUG);
- private static final long MINUTE_IN_MILLIS = 60 * 1000L;
-
private static final String ALARM_TAG_CLEANUP = "*job.cleanup*";
private static final String ALARM_TAG_QUOTA_CHECK = "*job.quota_check*";
@@ -244,6 +257,8 @@
public long expirationTimeElapsed;
public long windowSizeMs;
+ public int jobCountLimit;
+ public int sessionCountLimit;
/** The total amount of time the app ran in its respective bucket window size. */
public long executionTimeInWindowMs;
@@ -260,54 +275,58 @@
public int sessionCountInWindow;
/**
- * The time after which the sum of all the app's sessions plus {@link #mQuotaBufferMs}
- * equals the quota. This is only valid if
- * executionTimeInWindowMs >= {@link #mAllowedTimePerPeriodMs} or
- * executionTimeInMaxPeriodMs >= {@link #mMaxExecutionTimeMs}.
+ * The time after which the app will be under the bucket quota and can start running jobs
+ * again. This is only valid if
+ * {@link #executionTimeInWindowMs} >= {@link #mAllowedTimePerPeriodMs},
+ * {@link #executionTimeInMaxPeriodMs} >= {@link #mMaxExecutionTimeMs},
+ * {@link #bgJobCountInWindow} >= {@link #jobCountLimit}, or
+ * {@link #sessionCountInWindow} >= {@link #sessionCountLimit}.
*/
- public long quotaCutoffTimeElapsed;
+ public long inQuotaTimeElapsed;
/**
- * The time after which {@link #jobCountInAllowedTime} should be considered invalid, in the
- * elapsed realtime timebase.
+ * The time after which {@link #jobCountInRateLimitingWindow} should be considered invalid,
+ * in the elapsed realtime timebase.
*/
- public long jobCountExpirationTimeElapsed;
+ public long jobRateLimitExpirationTimeElapsed;
/**
- * The number of jobs that ran in at least the last {@link #mAllowedTimePerPeriodMs}.
+ * The number of jobs that ran in at least the last {@link #mRateLimitingWindowMs}.
* It may contain a few stale entries since cleanup won't happen exactly every
- * {@link #mAllowedTimePerPeriodMs}.
+ * {@link #mRateLimitingWindowMs}.
*/
- public int jobCountInAllowedTime;
+ public int jobCountInRateLimitingWindow;
/**
- * The time after which {@link #sessionCountInAllowedTime} should be considered
+ * The time after which {@link #sessionCountInRateLimitingWindow} should be considered
* invalid, in the elapsed realtime timebase.
*/
- public long sessionCountExpirationTimeElapsed;
+ public long sessionRateLimitExpirationTimeElapsed;
/**
* The number of {@link TimingSession}s that ran in at least the last
- * {@link #mAllowedTimePerPeriodMs}. It may contain a few stale entries since cleanup won't
- * happen exactly every {@link #mAllowedTimePerPeriodMs}. This should only be considered
- * valid before elapsed realtime has reached {@link #sessionCountExpirationTimeElapsed}.
+ * {@link #mRateLimitingWindowMs}. It may contain a few stale entries since cleanup won't
+ * happen exactly every {@link #mRateLimitingWindowMs}. This should only be considered
+ * valid before elapsed realtime has reached {@link #sessionRateLimitExpirationTimeElapsed}.
*/
- public int sessionCountInAllowedTime;
+ public int sessionCountInRateLimitingWindow;
@Override
public String toString() {
return "expirationTime=" + expirationTimeElapsed + ", "
- + "windowSize=" + windowSizeMs + ", "
+ + "windowSizeMs=" + windowSizeMs + ", "
+ + "jobCountLimit=" + jobCountLimit + ", "
+ + "sessionCountLimit=" + sessionCountLimit + ", "
+ "executionTimeInWindow=" + executionTimeInWindowMs + ", "
+ "bgJobCountInWindow=" + bgJobCountInWindow + ", "
+ "executionTimeInMaxPeriod=" + executionTimeInMaxPeriodMs + ", "
+ "bgJobCountInMaxPeriod=" + bgJobCountInMaxPeriod + ", "
+ "sessionCountInWindow=" + sessionCountInWindow + ", "
- + "quotaCutoffTime=" + quotaCutoffTimeElapsed + ", "
- + "jobCountExpirationTime=" + jobCountExpirationTimeElapsed + ", "
- + "jobCountInAllowedTime=" + jobCountInAllowedTime + ", "
- + "sessionCountExpirationTime=" + sessionCountExpirationTimeElapsed + ", "
- + "sessionCountInAllowedTime=" + sessionCountInAllowedTime;
+ + "inQuotaTime=" + inQuotaTimeElapsed + ", "
+ + "jobCountExpirationTime=" + jobRateLimitExpirationTimeElapsed + ", "
+ + "jobCountInRateLimitingWindow=" + jobCountInRateLimitingWindow + ", "
+ + "sessionCountExpirationTime=" + sessionRateLimitExpirationTimeElapsed + ", "
+ + "sessionCountInRateLimitingWindow=" + sessionCountInRateLimitingWindow;
}
@Override
@@ -316,18 +335,21 @@
ExecutionStats other = (ExecutionStats) obj;
return this.expirationTimeElapsed == other.expirationTimeElapsed
&& this.windowSizeMs == other.windowSizeMs
+ && this.jobCountLimit == other.jobCountLimit
+ && this.sessionCountLimit == other.sessionCountLimit
&& this.executionTimeInWindowMs == other.executionTimeInWindowMs
&& this.bgJobCountInWindow == other.bgJobCountInWindow
&& this.executionTimeInMaxPeriodMs == other.executionTimeInMaxPeriodMs
&& this.sessionCountInWindow == other.sessionCountInWindow
&& this.bgJobCountInMaxPeriod == other.bgJobCountInMaxPeriod
- && this.quotaCutoffTimeElapsed == other.quotaCutoffTimeElapsed
- && this.jobCountExpirationTimeElapsed == other.jobCountExpirationTimeElapsed
- && this.jobCountInAllowedTime == other.jobCountInAllowedTime
- && this.sessionCountExpirationTimeElapsed
- == other.sessionCountExpirationTimeElapsed
- && this.sessionCountInAllowedTime
- == other.sessionCountInAllowedTime;
+ && this.inQuotaTimeElapsed == other.inQuotaTimeElapsed
+ && this.jobRateLimitExpirationTimeElapsed
+ == other.jobRateLimitExpirationTimeElapsed
+ && this.jobCountInRateLimitingWindow == other.jobCountInRateLimitingWindow
+ && this.sessionRateLimitExpirationTimeElapsed
+ == other.sessionRateLimitExpirationTimeElapsed
+ && this.sessionCountInRateLimitingWindow
+ == other.sessionCountInRateLimitingWindow;
} else {
return false;
}
@@ -338,16 +360,18 @@
int result = 0;
result = 31 * result + hashLong(expirationTimeElapsed);
result = 31 * result + hashLong(windowSizeMs);
+ result = 31 * result + hashLong(jobCountLimit);
+ result = 31 * result + hashLong(sessionCountLimit);
result = 31 * result + hashLong(executionTimeInWindowMs);
result = 31 * result + bgJobCountInWindow;
result = 31 * result + hashLong(executionTimeInMaxPeriodMs);
result = 31 * result + bgJobCountInMaxPeriod;
result = 31 * result + sessionCountInWindow;
- result = 31 * result + hashLong(quotaCutoffTimeElapsed);
- result = 31 * result + hashLong(jobCountExpirationTimeElapsed);
- result = 31 * result + jobCountInAllowedTime;
- result = 31 * result + hashLong(sessionCountExpirationTimeElapsed);
- result = 31 * result + sessionCountInAllowedTime;
+ result = 31 * result + hashLong(inQuotaTimeElapsed);
+ result = 31 * result + hashLong(jobRateLimitExpirationTimeElapsed);
+ result = 31 * result + jobCountInRateLimitingWindow;
+ result = 31 * result + hashLong(sessionRateLimitExpirationTimeElapsed);
+ result = 31 * result + sessionCountInRateLimitingWindow;
return result;
}
}
@@ -399,19 +423,19 @@
private boolean mShouldThrottle;
/** How much time each app will have to run jobs within their standby bucket window. */
- private long mAllowedTimePerPeriodMs = 10 * MINUTE_IN_MILLIS;
+ private long mAllowedTimePerPeriodMs = QcConstants.DEFAULT_ALLOWED_TIME_PER_PERIOD_MS;
/**
* The maximum amount of time an app can have its jobs running within a {@link #MAX_PERIOD_MS}
* window.
*/
- private long mMaxExecutionTimeMs = 4 * 60 * MINUTE_IN_MILLIS;
+ private long mMaxExecutionTimeMs = QcConstants.DEFAULT_MAX_EXECUTION_TIME_MS;
/**
* How much time the app should have before transitioning from out-of-quota to in-quota.
* This should not affect processing if the app is already in-quota.
*/
- private long mQuotaBufferMs = 30 * 1000L; // 30 seconds
+ private long mQuotaBufferMs = QcConstants.DEFAULT_IN_QUOTA_BUFFER_MS;
/**
* {@link #mAllowedTimePerPeriodMs} - {@link #mQuotaBufferMs}. This can be used to determine
@@ -425,14 +449,19 @@
*/
private long mMaxExecutionTimeIntoQuotaMs = mMaxExecutionTimeMs - mQuotaBufferMs;
- /** The maximum number of jobs that can run within the past {@link #mAllowedTimePerPeriodMs}. */
- private int mMaxJobCountPerAllowedTime = 20;
+ /** The period of time used to rate limit recently run jobs. */
+ private long mRateLimitingWindowMs = QcConstants.DEFAULT_RATE_LIMITING_WINDOW_MS;
+
+ /** The maximum number of jobs that can run within the past {@link #mRateLimitingWindowMs}. */
+ private int mMaxJobCountPerRateLimitingWindow =
+ QcConstants.DEFAULT_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW;
/**
* The maximum number of {@link TimingSession}s that can run within the past {@link
- * #mAllowedTimePerPeriodMs}.
+ * #mRateLimitingWindowMs}.
*/
- private int mMaxSessionCountPerAllowedTime = 20;
+ private int mMaxSessionCountPerRateLimitingWindow =
+ QcConstants.DEFAULT_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW;
private long mNextCleanupTimeElapsed = 0;
private final AlarmManager.OnAlarmListener mSessionCleanupAlarmListener =
@@ -486,11 +515,11 @@
* The rolling window size for each standby bucket. Within each window, an app will have 10
* minutes to run its jobs.
*/
- private final long[] mBucketPeriodsMs = new long[] {
- 10 * MINUTE_IN_MILLIS, // 10 minutes for ACTIVE -- ACTIVE apps can run jobs at any time
- 2 * 60 * MINUTE_IN_MILLIS, // 2 hours for WORKING
- 8 * 60 * MINUTE_IN_MILLIS, // 8 hours for FREQUENT
- 24 * 60 * MINUTE_IN_MILLIS // 24 hours for RARE
+ private final long[] mBucketPeriodsMs = new long[]{
+ QcConstants.DEFAULT_WINDOW_SIZE_ACTIVE_MS,
+ QcConstants.DEFAULT_WINDOW_SIZE_WORKING_MS,
+ QcConstants.DEFAULT_WINDOW_SIZE_FREQUENT_MS,
+ QcConstants.DEFAULT_WINDOW_SIZE_RARE_MS
};
/** The maximum period any bucket can have. */
@@ -503,16 +532,13 @@
*
* @see #mBucketPeriodsMs
*/
- private final int[] mMaxBucketJobCounts = new int[] {
- 200, // ACTIVE -- 1200/hr
- 1200, // WORKING -- 600/hr
- 1800, // FREQUENT -- 225/hr
- 2400 // RARE -- 100/hr
+ private final int[] mMaxBucketJobCounts = new int[]{
+ QcConstants.DEFAULT_MAX_JOB_COUNT_ACTIVE,
+ QcConstants.DEFAULT_MAX_JOB_COUNT_WORKING,
+ QcConstants.DEFAULT_MAX_JOB_COUNT_FREQUENT,
+ QcConstants.DEFAULT_MAX_JOB_COUNT_RARE
};
- /** The minimum number of jobs that any bucket will be allowed to run. */
- private static final int MIN_BUCKET_JOB_COUNT = 100;
-
/**
* The maximum number of {@link TimingSession}s based on its standby bucket. For each max value
* count in the array, the app will not be allowed to have more than that many number of
@@ -527,14 +553,12 @@
QcConstants.DEFAULT_MAX_SESSION_COUNT_RARE
};
- /** The minimum number of {@link TimingSession}s that any bucket will be allowed to run. */
- private static final int MIN_BUCKET_SESSION_COUNT = 3;
-
/**
* Treat two distinct {@link TimingSession}s as the same if they start and end within this
* amount of time of each other.
*/
- private long mTimingSessionCoalescingDurationMs = 0;
+ private long mTimingSessionCoalescingDurationMs =
+ QcConstants.DEFAULT_TIMING_SESSION_COALESCING_DURATION_MS;
/** An app has reached its quota. The message should contain a {@link Package} object. */
private static final int MSG_REACHED_QUOTA = 0;
@@ -761,8 +785,8 @@
final int standbyBucket) {
final long now = sElapsedRealtimeClock.millis();
final boolean isUnderAllowedTimeQuota =
- (stats.jobCountExpirationTimeElapsed <= now
- || stats.jobCountInAllowedTime < mMaxJobCountPerAllowedTime);
+ (stats.jobRateLimitExpirationTimeElapsed <= now
+ || stats.jobCountInRateLimitingWindow < mMaxJobCountPerRateLimitingWindow);
return isUnderAllowedTimeQuota
&& (stats.bgJobCountInWindow < mMaxBucketJobCounts[standbyBucket]);
}
@@ -770,10 +794,8 @@
private boolean isUnderSessionCountQuotaLocked(@NonNull ExecutionStats stats,
final int standbyBucket) {
final long now = sElapsedRealtimeClock.millis();
- final boolean isUnderAllowedTimeQuota =
- (stats.sessionCountExpirationTimeElapsed <= now
- || stats.sessionCountInAllowedTime
- < mMaxSessionCountPerAllowedTime);
+ final boolean isUnderAllowedTimeQuota = (stats.sessionRateLimitExpirationTimeElapsed <= now
+ || stats.sessionCountInRateLimitingWindow < mMaxSessionCountPerRateLimitingWindow);
return isUnderAllowedTimeQuota
&& stats.sessionCountInWindow < mMaxBucketSessionCounts[standbyBucket];
}
@@ -924,12 +946,18 @@
}
if (refreshStatsIfOld) {
final long bucketWindowSizeMs = mBucketPeriodsMs[standbyBucket];
+ final int jobCountLimit = mMaxBucketJobCounts[standbyBucket];
+ final int sessionCountLimit = mMaxBucketSessionCounts[standbyBucket];
Timer timer = mPkgTimers.get(userId, packageName);
if ((timer != null && timer.isActive())
|| stats.expirationTimeElapsed <= sElapsedRealtimeClock.millis()
- || stats.windowSizeMs != bucketWindowSizeMs) {
+ || stats.windowSizeMs != bucketWindowSizeMs
+ || stats.jobCountLimit != jobCountLimit
+ || stats.sessionCountLimit != sessionCountLimit) {
// The stats are no longer valid.
stats.windowSizeMs = bucketWindowSizeMs;
+ stats.jobCountLimit = jobCountLimit;
+ stats.sessionCountLimit = sessionCountLimit;
updateExecutionStatsLocked(userId, packageName, stats);
}
}
@@ -945,7 +973,7 @@
stats.executionTimeInMaxPeriodMs = 0;
stats.bgJobCountInMaxPeriod = 0;
stats.sessionCountInWindow = 0;
- stats.quotaCutoffTimeElapsed = 0;
+ stats.inQuotaTimeElapsed = 0;
Timer timer = mPkgTimers.get(userId, packageName);
final long nowElapsed = sElapsedRealtimeClock.millis();
@@ -958,12 +986,12 @@
// invalidate now.
stats.expirationTimeElapsed = nowElapsed;
if (stats.executionTimeInWindowMs >= mAllowedTimeIntoQuotaMs) {
- stats.quotaCutoffTimeElapsed = Math.max(stats.quotaCutoffTimeElapsed,
- nowElapsed - mAllowedTimeIntoQuotaMs);
+ stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed,
+ nowElapsed - mAllowedTimeIntoQuotaMs + stats.windowSizeMs);
}
if (stats.executionTimeInMaxPeriodMs >= mMaxExecutionTimeIntoQuotaMs) {
- stats.quotaCutoffTimeElapsed = Math.max(stats.quotaCutoffTimeElapsed,
- nowElapsed - mMaxExecutionTimeIntoQuotaMs);
+ stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed,
+ nowElapsed - mMaxExecutionTimeIntoQuotaMs + MAX_PERIOD_MS);
}
}
@@ -985,36 +1013,40 @@
TimingSession session = sessions.get(i);
// Window management.
- if (startWindowElapsed < session.startTimeElapsed) {
- stats.executionTimeInWindowMs += session.endTimeElapsed - session.startTimeElapsed;
+ if (startWindowElapsed < session.endTimeElapsed) {
+ final long start;
+ if (startWindowElapsed < session.startTimeElapsed) {
+ start = session.startTimeElapsed;
+ emptyTimeMs =
+ Math.min(emptyTimeMs, session.startTimeElapsed - startWindowElapsed);
+ } else {
+ // The session started before the window but ended within the window. Only
+ // include the portion that was within the window.
+ start = startWindowElapsed;
+ emptyTimeMs = 0;
+ }
+
+ stats.executionTimeInWindowMs += session.endTimeElapsed - start;
stats.bgJobCountInWindow += session.bgJobCount;
- emptyTimeMs = Math.min(emptyTimeMs, session.startTimeElapsed - startWindowElapsed);
if (stats.executionTimeInWindowMs >= mAllowedTimeIntoQuotaMs) {
- stats.quotaCutoffTimeElapsed = Math.max(stats.quotaCutoffTimeElapsed,
- session.startTimeElapsed + stats.executionTimeInWindowMs
- - mAllowedTimeIntoQuotaMs);
+ stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed,
+ start + stats.executionTimeInWindowMs - mAllowedTimeIntoQuotaMs
+ + stats.windowSizeMs);
+ }
+ if (stats.bgJobCountInWindow >= stats.jobCountLimit) {
+ stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed,
+ session.endTimeElapsed + stats.windowSizeMs);
}
if (i == loopStart
|| (sessions.get(i + 1).startTimeElapsed - session.endTimeElapsed)
> mTimingSessionCoalescingDurationMs) {
// Coalesce sessions if they are very close to each other in time
sessionCountInWindow++;
- }
- } else if (startWindowElapsed < session.endTimeElapsed) {
- // The session started before the window but ended within the window. Only include
- // the portion that was within the window.
- stats.executionTimeInWindowMs += session.endTimeElapsed - startWindowElapsed;
- stats.bgJobCountInWindow += session.bgJobCount;
- emptyTimeMs = 0;
- if (stats.executionTimeInWindowMs >= mAllowedTimeIntoQuotaMs) {
- stats.quotaCutoffTimeElapsed = Math.max(stats.quotaCutoffTimeElapsed,
- startWindowElapsed + stats.executionTimeInWindowMs
- - mAllowedTimeIntoQuotaMs);
- }
- if (i == loopStart
- || (sessions.get(i + 1).startTimeElapsed - session.endTimeElapsed)
- > mTimingSessionCoalescingDurationMs) {
- sessionCountInWindow++;
+
+ if (sessionCountInWindow >= stats.sessionCountLimit) {
+ stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed,
+ session.endTimeElapsed + stats.windowSizeMs);
+ }
}
}
@@ -1025,9 +1057,9 @@
stats.bgJobCountInMaxPeriod += session.bgJobCount;
emptyTimeMs = Math.min(emptyTimeMs, session.startTimeElapsed - startMaxElapsed);
if (stats.executionTimeInMaxPeriodMs >= mMaxExecutionTimeIntoQuotaMs) {
- stats.quotaCutoffTimeElapsed = Math.max(stats.quotaCutoffTimeElapsed,
+ stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed,
session.startTimeElapsed + stats.executionTimeInMaxPeriodMs
- - mMaxExecutionTimeIntoQuotaMs);
+ - mMaxExecutionTimeIntoQuotaMs + MAX_PERIOD_MS);
}
} else if (startMaxElapsed < session.endTimeElapsed) {
// The session started before the window but ended within the window. Only include
@@ -1036,9 +1068,9 @@
stats.bgJobCountInMaxPeriod += session.bgJobCount;
emptyTimeMs = 0;
if (stats.executionTimeInMaxPeriodMs >= mMaxExecutionTimeIntoQuotaMs) {
- stats.quotaCutoffTimeElapsed = Math.max(stats.quotaCutoffTimeElapsed,
+ stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed,
startMaxElapsed + stats.executionTimeInMaxPeriodMs
- - mMaxExecutionTimeIntoQuotaMs);
+ - mMaxExecutionTimeIntoQuotaMs + MAX_PERIOD_MS);
}
} else {
// This session ended before the window. No point in going any further.
@@ -1094,11 +1126,11 @@
stats = new ExecutionStats();
appStats[i] = stats;
}
- if (stats.jobCountExpirationTimeElapsed <= now) {
- stats.jobCountExpirationTimeElapsed = now + mAllowedTimePerPeriodMs;
- stats.jobCountInAllowedTime = 0;
+ if (stats.jobRateLimitExpirationTimeElapsed <= now) {
+ stats.jobRateLimitExpirationTimeElapsed = now + mRateLimitingWindowMs;
+ stats.jobCountInRateLimitingWindow = 0;
}
- stats.jobCountInAllowedTime += count;
+ stats.jobCountInRateLimitingWindow += count;
}
}
@@ -1115,11 +1147,11 @@
stats = new ExecutionStats();
appStats[i] = stats;
}
- if (stats.sessionCountExpirationTimeElapsed <= now) {
- stats.sessionCountExpirationTimeElapsed = now + mAllowedTimePerPeriodMs;
- stats.sessionCountInAllowedTime = 0;
+ if (stats.sessionRateLimitExpirationTimeElapsed <= now) {
+ stats.sessionRateLimitExpirationTimeElapsed = now + mRateLimitingWindowMs;
+ stats.sessionCountInRateLimitingWindow = 0;
}
- stats.sessionCountInAllowedTime++;
+ stats.sessionCountInRateLimitingWindow++;
}
}
@@ -1367,18 +1399,17 @@
}
// The time this app will have quota again.
- long inQuotaTimeElapsed = stats.quotaCutoffTimeElapsed + stats.windowSizeMs;
- if (stats.executionTimeInMaxPeriodMs >= mMaxExecutionTimeMs) {
+ long inQuotaTimeElapsed = stats.inQuotaTimeElapsed;
+ if (!isUnderJobCountQuota && stats.bgJobCountInWindow < stats.jobCountLimit) {
+ // App hit the rate limit.
inQuotaTimeElapsed = Math.max(inQuotaTimeElapsed,
- stats.quotaCutoffTimeElapsed + MAX_PERIOD_MS);
+ stats.jobRateLimitExpirationTimeElapsed);
}
- if (!isUnderJobCountQuota) {
+ if (!isUnderTimingSessionCountQuota
+ && stats.sessionCountInWindow < stats.sessionCountLimit) {
+ // App hit the rate limit.
inQuotaTimeElapsed = Math.max(inQuotaTimeElapsed,
- stats.jobCountExpirationTimeElapsed + mAllowedTimePerPeriodMs);
- }
- if (!isUnderTimingSessionCountQuota) {
- inQuotaTimeElapsed = Math.max(inQuotaTimeElapsed,
- stats.sessionCountExpirationTimeElapsed + mAllowedTimePerPeriodMs);
+ stats.sessionRateLimitExpirationTimeElapsed);
}
// Only schedule the alarm if:
// 1. There isn't one currently scheduled
@@ -1399,7 +1430,7 @@
ALARM_TAG_QUOTA_CHECK, alarmListener, mHandler);
alarmListener.setTriggerTime(inQuotaTimeElapsed);
} else if (DEBUG) {
- Slog.d(TAG, "No need to scheduling start alarm for " + pkgString);
+ Slog.d(TAG, "No need to schedule start alarm for " + pkgString);
}
}
@@ -1968,14 +1999,15 @@
private static final String KEY_MAX_JOB_COUNT_WORKING = "max_job_count_working";
private static final String KEY_MAX_JOB_COUNT_FREQUENT = "max_job_count_frequent";
private static final String KEY_MAX_JOB_COUNT_RARE = "max_job_count_rare";
- private static final String KEY_MAX_JOB_COUNT_PER_ALLOWED_TIME =
- "max_count_per_allowed_time";
+ private static final String KEY_RATE_LIMITING_WINDOW_MS = "rate_limiting_window_ms";
+ private static final String KEY_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW =
+ "max_job_count_per_rate_limiting_window";
private static final String KEY_MAX_SESSION_COUNT_ACTIVE = "max_session_count_active";
private static final String KEY_MAX_SESSION_COUNT_WORKING = "max_session_count_working";
private static final String KEY_MAX_SESSION_COUNT_FREQUENT = "max_session_count_frequent";
private static final String KEY_MAX_SESSION_COUNT_RARE = "max_session_count_rare";
- private static final String KEY_MAX_SESSION_COUNT_PER_ALLOWED_TIME =
- "max_session_count_per_allowed_time";
+ private static final String KEY_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW =
+ "max_session_count_per_rate_limiting_window";
private static final String KEY_TIMING_SESSION_COALESCING_DURATION_MS =
"timing_session_coalescing_duration_ms";
@@ -1984,7 +2016,7 @@
private static final long DEFAULT_IN_QUOTA_BUFFER_MS =
30 * 1000L; // 30 seconds
private static final long DEFAULT_WINDOW_SIZE_ACTIVE_MS =
- 10 * 60 * 1000L; // 10 minutes for ACTIVE -- ACTIVE apps can run jobs at any time
+ DEFAULT_ALLOWED_TIME_PER_PERIOD_MS; // ACTIVE apps can run jobs at any time
private static final long DEFAULT_WINDOW_SIZE_WORKING_MS =
2 * 60 * 60 * 1000L; // 2 hours
private static final long DEFAULT_WINDOW_SIZE_FREQUENT_MS =
@@ -1992,16 +2024,18 @@
private static final long DEFAULT_WINDOW_SIZE_RARE_MS =
24 * 60 * 60 * 1000L; // 24 hours
private static final long DEFAULT_MAX_EXECUTION_TIME_MS =
- 4 * 60 * 60 * 1000L; // 4 hours
- private static final int DEFAULT_MAX_JOB_COUNT_ACTIVE =
- 200; // 1200/hr
- private static final int DEFAULT_MAX_JOB_COUNT_WORKING =
- 1200; // 600/hr
- private static final int DEFAULT_MAX_JOB_COUNT_FREQUENT =
- 1800; // 225/hr
- private static final int DEFAULT_MAX_JOB_COUNT_RARE =
- 2400; // 100/hr
- private static final int DEFAULT_MAX_JOB_COUNT_PER_ALLOWED_TIME = 20;
+ 4 * HOUR_IN_MILLIS;
+ private static final long DEFAULT_RATE_LIMITING_WINDOW_MS =
+ 10 * MINUTE_IN_MILLIS;
+ private static final int DEFAULT_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW = 20;
+ private static final int DEFAULT_MAX_JOB_COUNT_ACTIVE = // 20/window = 120/hr = 1/session
+ DEFAULT_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW;
+ private static final int DEFAULT_MAX_JOB_COUNT_WORKING = // 120/window = 60/hr = 12/session
+ (int) (60.0 * DEFAULT_WINDOW_SIZE_WORKING_MS / HOUR_IN_MILLIS);
+ private static final int DEFAULT_MAX_JOB_COUNT_FREQUENT = // 200/window = 25/hr = 25/session
+ (int) (25.0 * DEFAULT_WINDOW_SIZE_FREQUENT_MS / HOUR_IN_MILLIS);
+ private static final int DEFAULT_MAX_JOB_COUNT_RARE = // 48/window = 2/hr = 16/session
+ (int) (2.0 * DEFAULT_WINDOW_SIZE_RARE_MS / HOUR_IN_MILLIS);
private static final int DEFAULT_MAX_SESSION_COUNT_ACTIVE =
20; // 120/hr
private static final int DEFAULT_MAX_SESSION_COUNT_WORKING =
@@ -2010,8 +2044,8 @@
8; // 1/hr
private static final int DEFAULT_MAX_SESSION_COUNT_RARE =
3; // .125/hr
- private static final int DEFAULT_MAX_SESSION_COUNT_PER_ALLOWED_TIME = 20;
- private static final long DEFAULT_TIMING_SESSION_COALESCING_DURATION_MS = 0;
+ private static final int DEFAULT_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW = 20;
+ private static final long DEFAULT_TIMING_SESSION_COALESCING_DURATION_MS = 5000; // 5 seconds
/** How much time each app will have to run jobs within their standby bucket window. */
public long ALLOWED_TIME_PER_PERIOD_MS = DEFAULT_ALLOWED_TIME_PER_PERIOD_MS;
@@ -2079,11 +2113,14 @@
*/
public int MAX_JOB_COUNT_RARE = DEFAULT_MAX_JOB_COUNT_RARE;
+ /** The period of time used to rate limit recently run jobs. */
+ public long RATE_LIMITING_WINDOW_MS = DEFAULT_RATE_LIMITING_WINDOW_MS;
+
/**
- * The maximum number of jobs that can run within the past
- * {@link #ALLOWED_TIME_PER_PERIOD_MS}.
+ * The maximum number of jobs that can run within the past {@link #RATE_LIMITING_WINDOW_MS}.
*/
- public int MAX_JOB_COUNT_PER_ALLOWED_TIME = DEFAULT_MAX_JOB_COUNT_PER_ALLOWED_TIME;
+ public int MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW =
+ DEFAULT_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW;
/**
* The maximum number of {@link TimingSession}s an app can run within this particular
@@ -2113,7 +2150,8 @@
* The maximum number of {@link TimingSession}s that can run within the past
* {@link #ALLOWED_TIME_PER_PERIOD_MS}.
*/
- public int MAX_SESSION_COUNT_PER_ALLOWED_TIME = DEFAULT_MAX_SESSION_COUNT_PER_ALLOWED_TIME;
+ public int MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW =
+ DEFAULT_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW;
/**
* Treat two distinct {@link TimingSession}s as the same if they start and end within this
@@ -2122,6 +2160,29 @@
public long TIMING_SESSION_COALESCING_DURATION_MS =
DEFAULT_TIMING_SESSION_COALESCING_DURATION_MS;
+ // Safeguards
+
+ /** The minimum number of jobs that any bucket will be allowed to run within its window. */
+ private static final int MIN_BUCKET_JOB_COUNT = 10;
+
+ /**
+ * The minimum number of {@link TimingSession}s that any bucket will be allowed to run
+ * within its window.
+ */
+ private static final int MIN_BUCKET_SESSION_COUNT = 1;
+
+ /** The minimum value that {@link #MAX_EXECUTION_TIME_MS} can have. */
+ private static final long MIN_MAX_EXECUTION_TIME_MS = 60 * MINUTE_IN_MILLIS;
+
+ /** The minimum value that {@link #MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW} can have. */
+ private static final int MIN_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW = 10;
+
+ /** The minimum value that {@link #MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW} can have. */
+ private static final int MIN_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW = 10;
+
+ /** The minimum value that {@link #RATE_LIMITING_WINDOW_MS} can have. */
+ private static final long MIN_RATE_LIMITING_WINDOW_MS = 30 * SECOND_IN_MILLIS;
+
QcConstants(Handler handler) {
super(handler);
}
@@ -2167,8 +2228,11 @@
KEY_MAX_JOB_COUNT_FREQUENT, DEFAULT_MAX_JOB_COUNT_FREQUENT);
MAX_JOB_COUNT_RARE = mParser.getInt(
KEY_MAX_JOB_COUNT_RARE, DEFAULT_MAX_JOB_COUNT_RARE);
- MAX_JOB_COUNT_PER_ALLOWED_TIME = mParser.getInt(
- KEY_MAX_JOB_COUNT_PER_ALLOWED_TIME, DEFAULT_MAX_JOB_COUNT_PER_ALLOWED_TIME);
+ RATE_LIMITING_WINDOW_MS = mParser.getLong(
+ KEY_RATE_LIMITING_WINDOW_MS, DEFAULT_RATE_LIMITING_WINDOW_MS);
+ MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW = mParser.getInt(
+ KEY_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW,
+ DEFAULT_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW);
MAX_SESSION_COUNT_ACTIVE = mParser.getInt(
KEY_MAX_SESSION_COUNT_ACTIVE, DEFAULT_MAX_SESSION_COUNT_ACTIVE);
MAX_SESSION_COUNT_WORKING = mParser.getInt(
@@ -2177,9 +2241,9 @@
KEY_MAX_SESSION_COUNT_FREQUENT, DEFAULT_MAX_SESSION_COUNT_FREQUENT);
MAX_SESSION_COUNT_RARE = mParser.getInt(
KEY_MAX_SESSION_COUNT_RARE, DEFAULT_MAX_SESSION_COUNT_RARE);
- MAX_SESSION_COUNT_PER_ALLOWED_TIME = mParser.getInt(
- KEY_MAX_SESSION_COUNT_PER_ALLOWED_TIME,
- DEFAULT_MAX_SESSION_COUNT_PER_ALLOWED_TIME);
+ MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW = mParser.getInt(
+ KEY_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW,
+ DEFAULT_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW);
TIMING_SESSION_COALESCING_DURATION_MS = mParser.getLong(
KEY_TIMING_SESSION_COALESCING_DURATION_MS,
DEFAULT_TIMING_SESSION_COALESCING_DURATION_MS);
@@ -2192,7 +2256,14 @@
synchronized (mLock) {
boolean changed = false;
- long newAllowedTimeMs = Math.min(MAX_PERIOD_MS,
+ long newMaxExecutionTimeMs = Math.max(MIN_MAX_EXECUTION_TIME_MS,
+ Math.min(MAX_PERIOD_MS, MAX_EXECUTION_TIME_MS));
+ if (mMaxExecutionTimeMs != newMaxExecutionTimeMs) {
+ mMaxExecutionTimeMs = newMaxExecutionTimeMs;
+ mMaxExecutionTimeIntoQuotaMs = mMaxExecutionTimeMs - mQuotaBufferMs;
+ changed = true;
+ }
+ long newAllowedTimeMs = Math.min(mMaxExecutionTimeMs,
Math.max(MINUTE_IN_MILLIS, ALLOWED_TIME_PER_PERIOD_MS));
if (mAllowedTimePerPeriodMs != newAllowedTimeMs) {
mAllowedTimePerPeriodMs = newAllowedTimeMs;
@@ -2231,47 +2302,44 @@
mBucketPeriodsMs[RARE_INDEX] = newRarePeriodMs;
changed = true;
}
- long newMaxExecutionTimeMs = Math.max(60 * MINUTE_IN_MILLIS,
- Math.min(MAX_PERIOD_MS, MAX_EXECUTION_TIME_MS));
- if (mMaxExecutionTimeMs != newMaxExecutionTimeMs) {
- mMaxExecutionTimeMs = newMaxExecutionTimeMs;
- mMaxExecutionTimeIntoQuotaMs = mMaxExecutionTimeMs - mQuotaBufferMs;
+ long newRateLimitingWindowMs = Math.min(MAX_PERIOD_MS,
+ Math.max(MIN_RATE_LIMITING_WINDOW_MS, RATE_LIMITING_WINDOW_MS));
+ if (mRateLimitingWindowMs != newRateLimitingWindowMs) {
+ mRateLimitingWindowMs = newRateLimitingWindowMs;
changed = true;
}
- int newMaxCountPerAllowedPeriod = Math.max(10,
- MAX_JOB_COUNT_PER_ALLOWED_TIME);
- if (mMaxJobCountPerAllowedTime != newMaxCountPerAllowedPeriod) {
- mMaxJobCountPerAllowedTime = newMaxCountPerAllowedPeriod;
+ int newMaxJobCountPerRateLimitingWindow = Math.max(
+ MIN_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW,
+ MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW);
+ if (mMaxJobCountPerRateLimitingWindow != newMaxJobCountPerRateLimitingWindow) {
+ mMaxJobCountPerRateLimitingWindow = newMaxJobCountPerRateLimitingWindow;
changed = true;
}
- int newActiveMaxJobCount = Math.max(mMaxJobCountPerAllowedTime,
- Math.max(MIN_BUCKET_JOB_COUNT, MAX_JOB_COUNT_ACTIVE));
+ int newActiveMaxJobCount = Math.max(MIN_BUCKET_JOB_COUNT, MAX_JOB_COUNT_ACTIVE);
if (mMaxBucketJobCounts[ACTIVE_INDEX] != newActiveMaxJobCount) {
mMaxBucketJobCounts[ACTIVE_INDEX] = newActiveMaxJobCount;
changed = true;
}
- int newWorkingMaxJobCount = Math.max(mMaxJobCountPerAllowedTime,
- Math.max(MIN_BUCKET_JOB_COUNT, MAX_JOB_COUNT_WORKING));
+ int newWorkingMaxJobCount = Math.max(MIN_BUCKET_JOB_COUNT, MAX_JOB_COUNT_WORKING);
if (mMaxBucketJobCounts[WORKING_INDEX] != newWorkingMaxJobCount) {
mMaxBucketJobCounts[WORKING_INDEX] = newWorkingMaxJobCount;
changed = true;
}
- int newFrequentMaxJobCount = Math.max(mMaxJobCountPerAllowedTime,
- Math.max(MIN_BUCKET_JOB_COUNT, MAX_JOB_COUNT_FREQUENT));
+ int newFrequentMaxJobCount = Math.max(MIN_BUCKET_JOB_COUNT, MAX_JOB_COUNT_FREQUENT);
if (mMaxBucketJobCounts[FREQUENT_INDEX] != newFrequentMaxJobCount) {
mMaxBucketJobCounts[FREQUENT_INDEX] = newFrequentMaxJobCount;
changed = true;
}
- int newRareMaxJobCount = Math.max(mMaxJobCountPerAllowedTime,
- Math.max(MIN_BUCKET_JOB_COUNT, MAX_JOB_COUNT_RARE));
+ int newRareMaxJobCount = Math.max(MIN_BUCKET_JOB_COUNT, MAX_JOB_COUNT_RARE);
if (mMaxBucketJobCounts[RARE_INDEX] != newRareMaxJobCount) {
mMaxBucketJobCounts[RARE_INDEX] = newRareMaxJobCount;
changed = true;
}
- int newMaxSessionCountPerAllowedPeriod = Math.max(10,
- MAX_SESSION_COUNT_PER_ALLOWED_TIME);
- if (mMaxSessionCountPerAllowedTime != newMaxSessionCountPerAllowedPeriod) {
- mMaxSessionCountPerAllowedTime = newMaxSessionCountPerAllowedPeriod;
+ int newMaxSessionCountPerRateLimitPeriod = Math.max(
+ MIN_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW,
+ MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW);
+ if (mMaxSessionCountPerRateLimitingWindow != newMaxSessionCountPerRateLimitPeriod) {
+ mMaxSessionCountPerRateLimitingWindow = newMaxSessionCountPerRateLimitPeriod;
changed = true;
}
int newActiveMaxSessionCount =
@@ -2332,14 +2400,15 @@
pw.printPair(KEY_MAX_JOB_COUNT_WORKING, MAX_JOB_COUNT_WORKING).println();
pw.printPair(KEY_MAX_JOB_COUNT_FREQUENT, MAX_JOB_COUNT_FREQUENT).println();
pw.printPair(KEY_MAX_JOB_COUNT_RARE, MAX_JOB_COUNT_RARE).println();
- pw.printPair(KEY_MAX_JOB_COUNT_PER_ALLOWED_TIME, MAX_JOB_COUNT_PER_ALLOWED_TIME)
- .println();
+ pw.printPair(KEY_RATE_LIMITING_WINDOW_MS, RATE_LIMITING_WINDOW_MS).println();
+ pw.printPair(KEY_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW,
+ MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW).println();
pw.printPair(KEY_MAX_SESSION_COUNT_ACTIVE, MAX_SESSION_COUNT_ACTIVE).println();
pw.printPair(KEY_MAX_SESSION_COUNT_WORKING, MAX_SESSION_COUNT_WORKING).println();
pw.printPair(KEY_MAX_SESSION_COUNT_FREQUENT, MAX_SESSION_COUNT_FREQUENT).println();
pw.printPair(KEY_MAX_SESSION_COUNT_RARE, MAX_SESSION_COUNT_RARE).println();
- pw.printPair(KEY_MAX_SESSION_COUNT_PER_ALLOWED_TIME, MAX_SESSION_COUNT_PER_ALLOWED_TIME)
- .println();
+ pw.printPair(KEY_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW,
+ MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW).println();
pw.printPair(KEY_TIMING_SESSION_COALESCING_DURATION_MS,
TIMING_SESSION_COALESCING_DURATION_MS).println();
pw.decreaseIndent();
@@ -2365,8 +2434,10 @@
proto.write(ConstantsProto.QuotaController.MAX_JOB_COUNT_FREQUENT,
MAX_JOB_COUNT_FREQUENT);
proto.write(ConstantsProto.QuotaController.MAX_JOB_COUNT_RARE, MAX_JOB_COUNT_RARE);
- proto.write(ConstantsProto.QuotaController.MAX_JOB_COUNT_PER_ALLOWED_TIME,
- MAX_JOB_COUNT_PER_ALLOWED_TIME);
+ proto.write(ConstantsProto.QuotaController.RATE_LIMITING_WINDOW_MS,
+ RATE_LIMITING_WINDOW_MS);
+ proto.write(ConstantsProto.QuotaController.MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW,
+ MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW);
proto.write(ConstantsProto.QuotaController.MAX_SESSION_COUNT_ACTIVE,
MAX_SESSION_COUNT_ACTIVE);
proto.write(ConstantsProto.QuotaController.MAX_SESSION_COUNT_WORKING,
@@ -2375,8 +2446,8 @@
MAX_SESSION_COUNT_FREQUENT);
proto.write(ConstantsProto.QuotaController.MAX_SESSION_COUNT_RARE,
MAX_SESSION_COUNT_RARE);
- proto.write(ConstantsProto.QuotaController.MAX_SESSION_COUNT_PER_ALLOWED_TIME,
- MAX_SESSION_COUNT_PER_ALLOWED_TIME);
+ proto.write(ConstantsProto.QuotaController.MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW,
+ MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW);
proto.write(ConstantsProto.QuotaController.TIMING_SESSION_COALESCING_DURATION_MS,
TIMING_SESSION_COALESCING_DURATION_MS);
proto.end(qcToken);
@@ -2431,8 +2502,18 @@
}
@VisibleForTesting
- int getMaxJobCountPerAllowedTime() {
- return mMaxJobCountPerAllowedTime;
+ int getMaxJobCountPerRateLimitingWindow() {
+ return mMaxJobCountPerRateLimitingWindow;
+ }
+
+ @VisibleForTesting
+ int getMaxSessionCountPerRateLimitingWindow() {
+ return mMaxSessionCountPerRateLimitingWindow;
+ }
+
+ @VisibleForTesting
+ long getRateLimitingWindowMs() {
+ return mRateLimitingWindowMs;
}
@VisibleForTesting
@@ -2441,11 +2522,6 @@
}
@VisibleForTesting
- int getMaxSessionCountPerAllowedTime() {
- return mMaxSessionCountPerAllowedTime;
- }
-
- @VisibleForTesting
@Nullable
List<TimingSession> getTimingSessions(int userId, String packageName) {
return mTimingSessions.get(userId, packageName);
@@ -2659,6 +2735,12 @@
StateControllerProto.QuotaController.ExecutionStats.WINDOW_SIZE_MS,
es.windowSizeMs);
proto.write(
+ StateControllerProto.QuotaController.ExecutionStats.JOB_COUNT_LIMIT,
+ es.jobCountLimit);
+ proto.write(
+ StateControllerProto.QuotaController.ExecutionStats.SESSION_COUNT_LIMIT,
+ es.sessionCountLimit);
+ proto.write(
StateControllerProto.QuotaController.ExecutionStats.EXECUTION_TIME_IN_WINDOW_MS,
es.executionTimeInWindowMs);
proto.write(
@@ -2674,20 +2756,20 @@
StateControllerProto.QuotaController.ExecutionStats.SESSION_COUNT_IN_WINDOW,
es.sessionCountInWindow);
proto.write(
- StateControllerProto.QuotaController.ExecutionStats.QUOTA_CUTOFF_TIME_ELAPSED,
- es.quotaCutoffTimeElapsed);
+ StateControllerProto.QuotaController.ExecutionStats.IN_QUOTA_TIME_ELAPSED,
+ es.inQuotaTimeElapsed);
proto.write(
StateControllerProto.QuotaController.ExecutionStats.JOB_COUNT_EXPIRATION_TIME_ELAPSED,
- es.jobCountExpirationTimeElapsed);
+ es.jobRateLimitExpirationTimeElapsed);
proto.write(
- StateControllerProto.QuotaController.ExecutionStats.JOB_COUNT_IN_ALLOWED_TIME,
- es.jobCountInAllowedTime);
+ StateControllerProto.QuotaController.ExecutionStats.JOB_COUNT_IN_RATE_LIMITING_WINDOW,
+ es.jobCountInRateLimitingWindow);
proto.write(
StateControllerProto.QuotaController.ExecutionStats.SESSION_COUNT_EXPIRATION_TIME_ELAPSED,
- es.sessionCountExpirationTimeElapsed);
+ es.sessionRateLimitExpirationTimeElapsed);
proto.write(
- StateControllerProto.QuotaController.ExecutionStats.SESSION_COUNT_IN_ALLOWED_TIME,
- es.sessionCountInAllowedTime);
+ StateControllerProto.QuotaController.ExecutionStats.SESSION_COUNT_IN_RATE_LIMITING_WINDOW,
+ es.sessionCountInRateLimitingWindow);
proto.end(esToken);
}
}
diff --git a/services/core/java/com/android/server/net/NetworkStatsAccess.java b/services/core/java/com/android/server/net/NetworkStatsAccess.java
index cebc472..7c1c1c7 100644
--- a/services/core/java/com/android/server/net/NetworkStatsAccess.java
+++ b/services/core/java/com/android/server/net/NetworkStatsAccess.java
@@ -109,7 +109,7 @@
final TelephonyManager tm = (TelephonyManager)
context.getSystemService(Context.TELEPHONY_SERVICE);
boolean hasCarrierPrivileges = tm != null &&
- tm.checkCarrierPrivilegesForPackage(callingPackage) ==
+ tm.checkCarrierPrivilegesForPackageAnyPhone(callingPackage) ==
TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS;
boolean isDeviceOwner = dpmi != null && dpmi.isActiveAdminWithPolicy(callingUid,
DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
diff --git a/services/core/java/com/android/server/net/NetworkStatsFactory.java b/services/core/java/com/android/server/net/NetworkStatsFactory.java
index 69efd02..473cc97 100644
--- a/services/core/java/com/android/server/net/NetworkStatsFactory.java
+++ b/services/core/java/com/android/server/net/NetworkStatsFactory.java
@@ -263,6 +263,10 @@
return stats;
}
+ /**
+ * @deprecated Use NetworkStatsService#getDetailedUidStats which also accounts for
+ * VPN traffic
+ */
public NetworkStats readNetworkStatsDetail() throws IOException {
return readNetworkStatsDetail(UID_ALL, null, TAG_ALL, null);
}
diff --git a/services/core/java/com/android/server/net/NetworkStatsRecorder.java b/services/core/java/com/android/server/net/NetworkStatsRecorder.java
index a2e7e0c..bdff500 100644
--- a/services/core/java/com/android/server/net/NetworkStatsRecorder.java
+++ b/services/core/java/com/android/server/net/NetworkStatsRecorder.java
@@ -41,10 +41,10 @@
import com.android.internal.util.FileRotator;
import com.android.internal.util.IndentingPrintWriter;
-import libcore.io.IoUtils;
-
import com.google.android.collect.Sets;
+import libcore.io.IoUtils;
+
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.File;
@@ -234,7 +234,7 @@
if (vpnArray != null) {
for (VpnInfo info : vpnArray) {
- delta.migrateTun(info.ownerUid, info.vpnIface, info.primaryUnderlyingIface);
+ delta.migrateTun(info.ownerUid, info.vpnIface, info.underlyingIfaces);
}
}
diff --git a/services/core/java/com/android/server/net/NetworkStatsService.java b/services/core/java/com/android/server/net/NetworkStatsService.java
index f34ace5..a13368f 100644
--- a/services/core/java/com/android/server/net/NetworkStatsService.java
+++ b/services/core/java/com/android/server/net/NetworkStatsService.java
@@ -293,6 +293,22 @@
/** Data layer operation counters for splicing into other structures. */
private NetworkStats mUidOperations = new NetworkStats(0L, 10);
+ /**
+ * Snapshot containing most recent network stats for all UIDs across all interfaces and tags
+ * since boot.
+ *
+ * <p>Maintains migrated VPN stats which are result of performing TUN migration on {@link
+ * #mLastUidDetailSnapshot}.
+ */
+ @GuardedBy("mStatsLock")
+ private NetworkStats mTunAdjustedStats;
+ /**
+ * Used by {@link #mTunAdjustedStats} to migrate VPN traffic over delta between this snapshot
+ * and latest snapshot.
+ */
+ @GuardedBy("mStatsLock")
+ private NetworkStats mLastUidDetailSnapshot;
+
/** Must be set in factory by calling #setHandler. */
private Handler mHandler;
private Handler.Callback mHandlerCallback;
@@ -812,15 +828,39 @@
@Override
public NetworkStats getDetailedUidStats(String[] requiredIfaces) {
try {
+ // Get the latest snapshot from NetworkStatsFactory.
+ // TODO: Querying for INTERFACES_ALL may incur performance penalty. Consider restricting
+ // this to limited set of ifaces.
+ NetworkStats uidDetailStats = getNetworkStatsUidDetail(INTERFACES_ALL);
+
+ // Migrate traffic from VPN UID over delta and update mTunAdjustedStats.
+ NetworkStats result;
+ synchronized (mStatsLock) {
+ migrateTunTraffic(uidDetailStats, mVpnInfos);
+ result = mTunAdjustedStats.clone();
+ }
+
+ // Apply filter based on ifacesToQuery.
final String[] ifacesToQuery =
NetworkStatsFactory.augmentWithStackedInterfaces(requiredIfaces);
- return getNetworkStatsUidDetail(ifacesToQuery);
+ result.filter(UID_ALL, ifacesToQuery, TAG_ALL);
+ return result;
} catch (RemoteException e) {
Log.wtf(TAG, "Error compiling UID stats", e);
return new NetworkStats(0L, 0);
}
}
+ @VisibleForTesting
+ NetworkStats getTunAdjustedStats() {
+ synchronized (mStatsLock) {
+ if (mTunAdjustedStats == null) {
+ return null;
+ }
+ return mTunAdjustedStats.clone();
+ }
+ }
+
@Override
public String[] getMobileIfaces() {
return mMobileIfaces;
@@ -1295,6 +1335,34 @@
// a race condition between the service handler thread and the observer's
mStatsObservers.updateStats(xtSnapshot, uidSnapshot, new ArrayMap<>(mActiveIfaces),
new ArrayMap<>(mActiveUidIfaces), vpnArray, currentTime);
+
+ migrateTunTraffic(uidSnapshot, vpnArray);
+ }
+
+ /**
+ * Updates {@link #mTunAdjustedStats} with the delta containing traffic migrated off of VPNs.
+ */
+ @GuardedBy("mStatsLock")
+ private void migrateTunTraffic(NetworkStats uidDetailStats, VpnInfo[] vpnInfoArray) {
+ if (mTunAdjustedStats == null) {
+ // Either device booted or system server restarted, hence traffic cannot be migrated
+ // correctly without knowing the past state of VPN's underlying networks.
+ mTunAdjustedStats = uidDetailStats;
+ mLastUidDetailSnapshot = uidDetailStats;
+ return;
+ }
+ // Migrate delta traffic from VPN to other apps.
+ NetworkStats delta = uidDetailStats.subtract(mLastUidDetailSnapshot);
+ for (VpnInfo info : vpnInfoArray) {
+ delta.migrateTun(info.ownerUid, info.vpnIface, info.underlyingIfaces);
+ }
+ // Filter out debug entries as that may lead to over counting.
+ delta.filterDebugEntries();
+ // Update #mTunAdjustedStats with migrated delta.
+ mTunAdjustedStats.combineAllValues(delta);
+ mTunAdjustedStats.setElapsedRealtime(uidDetailStats.getElapsedRealtime());
+ // Update last snapshot.
+ mLastUidDetailSnapshot = uidDetailStats;
}
/**
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 2a61fee..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);
}
}
@@ -17080,6 +17082,13 @@
cleanUpAppIdCreation(result);
}
}
+ // TODO(patb): create a more descriptive reason than unknown in future release
+ // mark all non-failure installs as UNKNOWN so we do not treat them as success
+ for (InstallRequest request : requests) {
+ if (request.installResult.returnCode == PackageManager.INSTALL_SUCCEEDED) {
+ request.installResult.returnCode = PackageManager.INSTALL_UNKNOWN;
+ }
+ }
}
for (PrepareResult result : prepareResults.values()) {
if (result.freezer != null) {
diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
index f5c8049..81de8e2 100644
--- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
+++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
@@ -2496,9 +2496,9 @@
default:
throw new IllegalArgumentException("Unknown option " + opt);
}
- if (replaceExisting) {
- sessionParams.installFlags |= PackageManager.INSTALL_REPLACE_EXISTING;
- }
+ }
+ if (replaceExisting) {
+ sessionParams.installFlags |= PackageManager.INSTALL_REPLACE_EXISTING;
}
return params;
}
diff --git a/services/core/java/com/android/server/pm/StagingManager.java b/services/core/java/com/android/server/pm/StagingManager.java
index 24bf18d..950450c 100644
--- a/services/core/java/com/android/server/pm/StagingManager.java
+++ b/services/core/java/com/android/server/pm/StagingManager.java
@@ -36,7 +36,6 @@
import android.content.pm.ParceledListSlice;
import android.content.pm.Signature;
import android.content.rollback.IRollbackManager;
-import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
@@ -116,12 +115,9 @@
final PackageInfo packageInfo = mApexManager.getPackageInfoForApexName(packageName);
if (packageInfo == null) {
- // Only allow installing new apexes if on a debuggable build.
- if (!Build.IS_DEBUGGABLE) {
- Slog.w(TAG, "Attempted to install new apex " + packageName + " on user build");
- return false;
- }
- return true;
+ // Don't allow installation of new APEX.
+ Slog.e(TAG, "Attempted to install a new apex " + packageName + ". Rejecting");
+ return false;
}
final SigningDetails existingSigningDetails;
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/webkit/WebViewUpdateServiceImpl.java b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl.java
index 476a273..165055a 100644
--- a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl.java
+++ b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl.java
@@ -19,6 +19,7 @@
import android.content.pm.PackageInfo;
import android.os.AsyncTask;
import android.os.UserHandle;
+import android.util.Slog;
import android.webkit.WebViewProviderInfo;
import android.webkit.WebViewProviderResponse;
@@ -154,7 +155,10 @@
WebViewProviderInfo[] webviewProviders = mSystemInterface.getWebViewPackages();
WebViewProviderInfo fallbackProvider = getFallbackProvider(webviewProviders);
if (fallbackProvider != null) {
+ Slog.i(TAG, "One-time migration: enabling " + fallbackProvider.packageName);
mSystemInterface.enablePackageForAllUsers(mContext, fallbackProvider.packageName, true);
+ } else {
+ Slog.i(TAG, "Skipping one-time migration: no fallback provider");
}
mSystemInterface.enableFallbackLogic(false);
}
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/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index e1dd352..21f01ff 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -2720,9 +2720,9 @@
res.getBoolean(R.bool.config_navBarAlwaysShowOnSideEdgeGesture);
// This should calculate how much above the frame we accept gestures.
- mBottomGestureAdditionalInset = Math.max(0,
+ mBottomGestureAdditionalInset =
res.getDimensionPixelSize(R.dimen.navigation_bar_gesture_height)
- - getNavigationBarFrameHeight(portraitRotation, uiMode));
+ - getNavigationBarFrameHeight(portraitRotation, uiMode);
updateConfigurationAndScreenSizeDependentBehaviors();
mWindowOutsetBottom = ScreenShapeHelper.getWindowOutsetBottomPx(mContext.getResources());
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/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
index 4e89357..1db6b8e 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
@@ -386,6 +386,8 @@
ExecutionStats expectedStats = new ExecutionStats();
expectedStats.expirationTimeElapsed = now + 24 * HOUR_IN_MILLIS;
expectedStats.windowSizeMs = 24 * HOUR_IN_MILLIS;
+ expectedStats.jobCountLimit = mQcConstants.MAX_JOB_COUNT_RARE;
+ expectedStats.sessionCountLimit = mQcConstants.MAX_SESSION_COUNT_RARE;
final int uid = 10001;
mQuotaController.onAppRemovedLocked("com.android.test.remove", uid);
@@ -424,6 +426,8 @@
ExecutionStats expectedStats = new ExecutionStats();
expectedStats.expirationTimeElapsed = now + 24 * HOUR_IN_MILLIS;
expectedStats.windowSizeMs = 24 * HOUR_IN_MILLIS;
+ expectedStats.jobCountLimit = mQcConstants.MAX_JOB_COUNT_RARE;
+ expectedStats.sessionCountLimit = mQcConstants.MAX_SESSION_COUNT_RARE;
mQuotaController.onUserRemovedLocked(0);
assertNull(mQuotaController.getTimingSessions(0, "com.android.test"));
@@ -456,6 +460,8 @@
ExecutionStats inputStats = new ExecutionStats();
inputStats.windowSizeMs = expectedStats.windowSizeMs = 12 * HOUR_IN_MILLIS;
+ inputStats.jobCountLimit = expectedStats.jobCountLimit = 100;
+ inputStats.sessionCountLimit = expectedStats.sessionCountLimit = 100;
// Invalid time is now +24 hours since there are no sessions at all for the app.
expectedStats.expirationTimeElapsed = now + 24 * HOUR_IN_MILLIS;
mQuotaController.updateExecutionStatsLocked(0, "com.android.test.not.run", inputStats);
@@ -520,6 +526,7 @@
assertEquals(expectedStats, inputStats);
inputStats.windowSizeMs = expectedStats.windowSizeMs = HOUR_IN_MILLIS;
+ inputStats.sessionCountLimit = expectedStats.sessionCountLimit = 2;
// Invalid time is now since the start of the session is at the very edge of the window
// cutoff time.
expectedStats.expirationTimeElapsed = now;
@@ -528,10 +535,13 @@
expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS;
expectedStats.bgJobCountInMaxPeriod = 15;
expectedStats.sessionCountInWindow = 3;
+ expectedStats.inQuotaTimeElapsed = now + 11 * MINUTE_IN_MILLIS;
mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats);
assertEquals(expectedStats, inputStats);
inputStats.windowSizeMs = expectedStats.windowSizeMs = 2 * HOUR_IN_MILLIS;
+ inputStats.jobCountLimit = expectedStats.jobCountLimit = 6;
+ inputStats.sessionCountLimit = expectedStats.sessionCountLimit = 100;
// Invalid time is now since the session straddles the window cutoff time.
expectedStats.expirationTimeElapsed = now;
expectedStats.executionTimeInWindowMs = 11 * MINUTE_IN_MILLIS;
@@ -539,8 +549,7 @@
expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS;
expectedStats.bgJobCountInMaxPeriod = 15;
expectedStats.sessionCountInWindow = 4;
- expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - MINUTE_IN_MILLIS)
- + mQcConstants.IN_QUOTA_BUFFER_MS;
+ expectedStats.inQuotaTimeElapsed = now + 5 * MINUTE_IN_MILLIS;
mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats);
assertEquals(expectedStats, inputStats);
@@ -553,8 +562,9 @@
expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS;
expectedStats.bgJobCountInMaxPeriod = 15;
expectedStats.sessionCountInWindow = 4;
- expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - MINUTE_IN_MILLIS)
- + mQcConstants.IN_QUOTA_BUFFER_MS;
+ // App goes under job execution time limit in ~61 minutes, but will be under job count limit
+ // in 65 minutes.
+ expectedStats.inQuotaTimeElapsed = now + 65 * MINUTE_IN_MILLIS;
mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats);
assertEquals(expectedStats, inputStats);
@@ -567,8 +577,7 @@
expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS;
expectedStats.bgJobCountInMaxPeriod = 15;
expectedStats.sessionCountInWindow = 5;
- expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - MINUTE_IN_MILLIS)
- + mQcConstants.IN_QUOTA_BUFFER_MS;
+ expectedStats.inQuotaTimeElapsed = now + 4 * HOUR_IN_MILLIS + 5 * MINUTE_IN_MILLIS;
mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats);
assertEquals(expectedStats, inputStats);
@@ -577,6 +586,7 @@
.add(0,
createTimingSession(now - (23 * HOUR_IN_MILLIS), MINUTE_IN_MILLIS, 3));
inputStats.windowSizeMs = expectedStats.windowSizeMs = 8 * HOUR_IN_MILLIS;
+ inputStats.jobCountLimit = expectedStats.jobCountLimit = 100;
// Invalid time is now +1 hour since the earliest session in the max period is 1 hour
// before the end of the max period cutoff time.
expectedStats.expirationTimeElapsed = now + HOUR_IN_MILLIS;
@@ -585,7 +595,7 @@
expectedStats.executionTimeInMaxPeriodMs = 23 * MINUTE_IN_MILLIS;
expectedStats.bgJobCountInMaxPeriod = 18;
expectedStats.sessionCountInWindow = 5;
- expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - MINUTE_IN_MILLIS)
+ expectedStats.inQuotaTimeElapsed = now + 6 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS
+ mQcConstants.IN_QUOTA_BUFFER_MS;
mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats);
assertEquals(expectedStats, inputStats);
@@ -602,7 +612,7 @@
expectedStats.executionTimeInMaxPeriodMs = 24 * MINUTE_IN_MILLIS;
expectedStats.bgJobCountInMaxPeriod = 20;
expectedStats.sessionCountInWindow = 5;
- expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - MINUTE_IN_MILLIS)
+ expectedStats.inQuotaTimeElapsed = now + 6 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS
+ mQcConstants.IN_QUOTA_BUFFER_MS;
mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats);
assertEquals(expectedStats, inputStats);
@@ -627,6 +637,8 @@
// Active
expectedStats.windowSizeMs = 10 * MINUTE_IN_MILLIS;
+ expectedStats.jobCountLimit = mQcConstants.MAX_JOB_COUNT_ACTIVE;
+ expectedStats.sessionCountLimit = mQcConstants.MAX_SESSION_COUNT_ACTIVE;
expectedStats.expirationTimeElapsed = now + 4 * MINUTE_IN_MILLIS;
expectedStats.executionTimeInWindowMs = 3 * MINUTE_IN_MILLIS;
expectedStats.bgJobCountInWindow = 5;
@@ -638,45 +650,103 @@
// Working
expectedStats.windowSizeMs = 2 * HOUR_IN_MILLIS;
+ expectedStats.jobCountLimit = mQcConstants.MAX_JOB_COUNT_WORKING;
+ expectedStats.sessionCountLimit = mQcConstants.MAX_SESSION_COUNT_WORKING;
expectedStats.expirationTimeElapsed = now;
expectedStats.executionTimeInWindowMs = 13 * MINUTE_IN_MILLIS;
expectedStats.bgJobCountInWindow = 10;
expectedStats.executionTimeInMaxPeriodMs = 33 * MINUTE_IN_MILLIS;
expectedStats.bgJobCountInMaxPeriod = 20;
expectedStats.sessionCountInWindow = 2;
- expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - 3 * MINUTE_IN_MILLIS)
+ expectedStats.inQuotaTimeElapsed = now + 3 * MINUTE_IN_MILLIS
+ mQcConstants.IN_QUOTA_BUFFER_MS;
assertEquals(expectedStats,
mQuotaController.getExecutionStatsLocked(0, "com.android.test", WORKING_INDEX));
// Frequent
expectedStats.windowSizeMs = 8 * HOUR_IN_MILLIS;
+ expectedStats.jobCountLimit = mQcConstants.MAX_JOB_COUNT_FREQUENT;
+ expectedStats.sessionCountLimit = mQcConstants.MAX_SESSION_COUNT_FREQUENT;
expectedStats.expirationTimeElapsed = now + HOUR_IN_MILLIS;
expectedStats.executionTimeInWindowMs = 23 * MINUTE_IN_MILLIS;
expectedStats.bgJobCountInWindow = 15;
expectedStats.executionTimeInMaxPeriodMs = 33 * MINUTE_IN_MILLIS;
expectedStats.bgJobCountInMaxPeriod = 20;
expectedStats.sessionCountInWindow = 3;
- expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - 3 * MINUTE_IN_MILLIS)
+ expectedStats.inQuotaTimeElapsed = now + 6 * HOUR_IN_MILLIS + 3 * MINUTE_IN_MILLIS
+ mQcConstants.IN_QUOTA_BUFFER_MS;
assertEquals(expectedStats,
mQuotaController.getExecutionStatsLocked(0, "com.android.test", FREQUENT_INDEX));
// Rare
expectedStats.windowSizeMs = 24 * HOUR_IN_MILLIS;
+ expectedStats.jobCountLimit = mQcConstants.MAX_JOB_COUNT_RARE;
+ expectedStats.sessionCountLimit = mQcConstants.MAX_SESSION_COUNT_RARE;
expectedStats.expirationTimeElapsed = now + HOUR_IN_MILLIS;
expectedStats.executionTimeInWindowMs = 33 * MINUTE_IN_MILLIS;
expectedStats.bgJobCountInWindow = 20;
expectedStats.executionTimeInMaxPeriodMs = 33 * MINUTE_IN_MILLIS;
expectedStats.bgJobCountInMaxPeriod = 20;
expectedStats.sessionCountInWindow = 4;
- expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - 3 * MINUTE_IN_MILLIS)
+ expectedStats.inQuotaTimeElapsed = now + 22 * HOUR_IN_MILLIS + 3 * MINUTE_IN_MILLIS
+ mQcConstants.IN_QUOTA_BUFFER_MS;
assertEquals(expectedStats,
mQuotaController.getExecutionStatsLocked(0, "com.android.test", RARE_INDEX));
}
/**
+ * Tests that getExecutionStatsLocked returns the correct stats soon after device startup.
+ */
+ @Test
+ public void testGetExecutionStatsLocked_Values_BeginningOfTime() {
+ // Set time to 3 minutes after boot.
+ advanceElapsedClock(-JobSchedulerService.sElapsedRealtimeClock.millis());
+ advanceElapsedClock(3 * MINUTE_IN_MILLIS);
+
+ mQuotaController.saveTimingSession(0, "com.android.test",
+ createTimingSession(MINUTE_IN_MILLIS, MINUTE_IN_MILLIS, 2));
+
+ ExecutionStats expectedStats = new ExecutionStats();
+
+ // Active
+ expectedStats.windowSizeMs = 10 * MINUTE_IN_MILLIS;
+ expectedStats.jobCountLimit = mQcConstants.MAX_JOB_COUNT_ACTIVE;
+ expectedStats.sessionCountLimit = mQcConstants.MAX_SESSION_COUNT_ACTIVE;
+ expectedStats.expirationTimeElapsed = 11 * MINUTE_IN_MILLIS;
+ expectedStats.executionTimeInWindowMs = MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInWindow = 2;
+ expectedStats.executionTimeInMaxPeriodMs = MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInMaxPeriod = 2;
+ expectedStats.sessionCountInWindow = 1;
+ assertEquals(expectedStats,
+ mQuotaController.getExecutionStatsLocked(0, "com.android.test", ACTIVE_INDEX));
+
+ // Working
+ expectedStats.windowSizeMs = 2 * HOUR_IN_MILLIS;
+ expectedStats.jobCountLimit = mQcConstants.MAX_JOB_COUNT_WORKING;
+ expectedStats.sessionCountLimit = mQcConstants.MAX_SESSION_COUNT_WORKING;
+ expectedStats.expirationTimeElapsed = 2 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS;
+ assertEquals(expectedStats,
+ mQuotaController.getExecutionStatsLocked(0, "com.android.test", WORKING_INDEX));
+
+ // Frequent
+ expectedStats.windowSizeMs = 8 * HOUR_IN_MILLIS;
+ expectedStats.jobCountLimit = mQcConstants.MAX_JOB_COUNT_FREQUENT;
+ expectedStats.sessionCountLimit = mQcConstants.MAX_SESSION_COUNT_FREQUENT;
+ expectedStats.expirationTimeElapsed = 8 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS;
+ assertEquals(expectedStats,
+ mQuotaController.getExecutionStatsLocked(0, "com.android.test", FREQUENT_INDEX));
+
+ // Rare
+ expectedStats.windowSizeMs = 24 * HOUR_IN_MILLIS;
+ expectedStats.jobCountLimit = mQcConstants.MAX_JOB_COUNT_RARE;
+ expectedStats.sessionCountLimit = mQcConstants.MAX_SESSION_COUNT_RARE;
+ expectedStats.expirationTimeElapsed = 24 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS;
+ assertEquals(expectedStats,
+ mQuotaController.getExecutionStatsLocked(0, "com.android.test", RARE_INDEX));
+ }
+
+ /**
* Tests that getExecutionStatsLocked returns the correct timing session stats when coalescing.
*/
@Test
@@ -850,13 +920,15 @@
ExecutionStats expectedStats = new ExecutionStats();
expectedStats.windowSizeMs = originalStatsActive.windowSizeMs;
+ expectedStats.jobCountLimit = originalStatsActive.jobCountLimit;
+ expectedStats.sessionCountLimit = originalStatsActive.sessionCountLimit;
expectedStats.expirationTimeElapsed = originalStatsActive.expirationTimeElapsed;
expectedStats.executionTimeInWindowMs = originalStatsActive.executionTimeInWindowMs;
expectedStats.bgJobCountInWindow = originalStatsActive.bgJobCountInWindow;
expectedStats.executionTimeInMaxPeriodMs = originalStatsActive.executionTimeInMaxPeriodMs;
expectedStats.bgJobCountInMaxPeriod = originalStatsActive.bgJobCountInMaxPeriod;
expectedStats.sessionCountInWindow = originalStatsActive.sessionCountInWindow;
- expectedStats.quotaCutoffTimeElapsed = originalStatsActive.quotaCutoffTimeElapsed;
+ expectedStats.inQuotaTimeElapsed = originalStatsActive.inQuotaTimeElapsed;
final ExecutionStats newStatsActive = mQuotaController.getExecutionStatsLocked(0,
"com.android.test", ACTIVE_INDEX);
// Stats for the same bucket should use the same object.
@@ -864,33 +936,39 @@
assertEquals(expectedStats, newStatsActive);
expectedStats.windowSizeMs = originalStatsWorking.windowSizeMs;
+ expectedStats.jobCountLimit = originalStatsWorking.jobCountLimit;
+ expectedStats.sessionCountLimit = originalStatsWorking.sessionCountLimit;
expectedStats.expirationTimeElapsed = originalStatsWorking.expirationTimeElapsed;
expectedStats.executionTimeInWindowMs = originalStatsWorking.executionTimeInWindowMs;
expectedStats.bgJobCountInWindow = originalStatsWorking.bgJobCountInWindow;
expectedStats.sessionCountInWindow = originalStatsWorking.sessionCountInWindow;
- expectedStats.quotaCutoffTimeElapsed = originalStatsWorking.quotaCutoffTimeElapsed;
+ expectedStats.inQuotaTimeElapsed = originalStatsWorking.inQuotaTimeElapsed;
final ExecutionStats newStatsWorking = mQuotaController.getExecutionStatsLocked(0,
"com.android.test", WORKING_INDEX);
assertTrue(originalStatsWorking == newStatsWorking);
assertNotEquals(expectedStats, newStatsWorking);
expectedStats.windowSizeMs = originalStatsFrequent.windowSizeMs;
+ expectedStats.jobCountLimit = originalStatsFrequent.jobCountLimit;
+ expectedStats.sessionCountLimit = originalStatsFrequent.sessionCountLimit;
expectedStats.expirationTimeElapsed = originalStatsFrequent.expirationTimeElapsed;
expectedStats.executionTimeInWindowMs = originalStatsFrequent.executionTimeInWindowMs;
expectedStats.bgJobCountInWindow = originalStatsFrequent.bgJobCountInWindow;
expectedStats.sessionCountInWindow = originalStatsFrequent.sessionCountInWindow;
- expectedStats.quotaCutoffTimeElapsed = originalStatsFrequent.quotaCutoffTimeElapsed;
+ expectedStats.inQuotaTimeElapsed = originalStatsFrequent.inQuotaTimeElapsed;
final ExecutionStats newStatsFrequent = mQuotaController.getExecutionStatsLocked(0,
"com.android.test", FREQUENT_INDEX);
assertTrue(originalStatsFrequent == newStatsFrequent);
assertNotEquals(expectedStats, newStatsFrequent);
expectedStats.windowSizeMs = originalStatsRare.windowSizeMs;
+ expectedStats.jobCountLimit = originalStatsRare.jobCountLimit;
+ expectedStats.sessionCountLimit = originalStatsRare.sessionCountLimit;
expectedStats.expirationTimeElapsed = originalStatsRare.expirationTimeElapsed;
expectedStats.executionTimeInWindowMs = originalStatsRare.executionTimeInWindowMs;
expectedStats.bgJobCountInWindow = originalStatsRare.bgJobCountInWindow;
expectedStats.sessionCountInWindow = originalStatsRare.sessionCountInWindow;
- expectedStats.quotaCutoffTimeElapsed = originalStatsRare.quotaCutoffTimeElapsed;
+ expectedStats.inQuotaTimeElapsed = originalStatsRare.inQuotaTimeElapsed;
final ExecutionStats newStatsRare = mQuotaController.getExecutionStatsLocked(0,
"com.android.test", RARE_INDEX);
assertTrue(originalStatsRare == newStatsRare);
@@ -1065,7 +1143,7 @@
public void testIsWithinQuotaLocked_UnderDuration_OverJobCount() {
setDischarging();
final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
- final int jobCount = mQcConstants.MAX_JOB_COUNT_PER_ALLOWED_TIME;
+ final int jobCount = mQcConstants.MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW;
mQuotaController.saveTimingSession(0, "com.android.test.spam",
createTimingSession(now - (HOUR_IN_MILLIS), 15 * MINUTE_IN_MILLIS, 25));
mQuotaController.saveTimingSession(0, "com.android.test.spam",
@@ -1100,7 +1178,7 @@
public void testIsWithinQuotaLocked_OverDuration_OverJobCount() {
setDischarging();
final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
- final int jobCount = mQcConstants.MAX_JOB_COUNT_PER_ALLOWED_TIME;
+ final int jobCount = mQcConstants.MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW;
mQuotaController.saveTimingSession(0, "com.android.test",
createTimingSession(now - (HOUR_IN_MILLIS), 15 * MINUTE_IN_MILLIS, 25));
mQuotaController.saveTimingSession(0, "com.android.test",
@@ -1141,7 +1219,7 @@
advanceElapsedClock(10 * MINUTE_IN_MILLIS + 30 * SECOND_IN_MILLIS);
assertEquals(2, mQuotaController.getExecutionStatsLocked(
- SOURCE_USER_ID, SOURCE_PACKAGE, ACTIVE_INDEX).jobCountInAllowedTime);
+ SOURCE_USER_ID, SOURCE_PACKAGE, ACTIVE_INDEX).jobCountInRateLimitingWindow);
assertTrue(mQuotaController.isWithinQuotaLocked(jobStatus));
}
@@ -1212,9 +1290,9 @@
mQuotaController.getTimingSessions(SOURCE_USER_ID, fgChangerPkgName).size());
for (int i = ACTIVE_INDEX; i < RARE_INDEX; ++i) {
assertEquals(42, mQuotaController.getExecutionStatsLocked(
- SOURCE_USER_ID, fgChangerPkgName, i).jobCountInAllowedTime);
+ SOURCE_USER_ID, fgChangerPkgName, i).jobCountInRateLimitingWindow);
assertEquals(1, mQuotaController.getExecutionStatsLocked(
- SOURCE_USER_ID, unaffectedPkgName, i).jobCountInAllowedTime);
+ SOURCE_USER_ID, unaffectedPkgName, i).jobCountInRateLimitingWindow);
}
}
@@ -1555,26 +1633,29 @@
}
@Test
- public void testMaybeScheduleStartAlarmLocked_JobCount_AllowedTime() {
+ public void testMaybeScheduleStartAlarmLocked_JobCount_RateLimitingWindow() {
+ // Set rate limiting period different from allowed time to confirm code sets based on
+ // the former.
+ mQcConstants.ALLOWED_TIME_PER_PERIOD_MS = 10 * MINUTE_IN_MILLIS;
+ mQcConstants.RATE_LIMITING_WINDOW_MS = 5 * MINUTE_IN_MILLIS;
+ mQcConstants.updateConstants();
+
final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
final int standbyBucket = WORKING_INDEX;
ExecutionStats stats = mQuotaController.getExecutionStatsLocked(SOURCE_USER_ID,
SOURCE_PACKAGE, standbyBucket);
- stats.jobCountInAllowedTime =
- mQcConstants.MAX_JOB_COUNT_PER_ALLOWED_TIME + 2;
+ stats.jobCountInRateLimitingWindow =
+ mQcConstants.MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW + 2;
// Invalid time in the past, so the count shouldn't be used.
- stats.jobCountExpirationTimeElapsed =
- now - mQuotaController.getAllowedTimePerPeriodMs() / 2;
+ stats.jobRateLimitExpirationTimeElapsed = now - 5 * MINUTE_IN_MILLIS / 2;
mQuotaController.maybeScheduleStartAlarmLocked(
SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
- // Invalid time in the future, so the count should be used.
- stats.jobCountExpirationTimeElapsed =
- now + mQuotaController.getAllowedTimePerPeriodMs() / 2;
- final long expectedWorkingAlarmTime =
- stats.jobCountExpirationTimeElapsed + mQuotaController.getAllowedTimePerPeriodMs();
+ // Valid time in the future, so the count should be used.
+ stats.jobRateLimitExpirationTimeElapsed = now + 5 * MINUTE_IN_MILLIS / 2;
+ final long expectedWorkingAlarmTime = stats.jobRateLimitExpirationTimeElapsed;
mQuotaController.maybeScheduleStartAlarmLocked(
SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
verify(mAlarmManager, times(1))
@@ -1732,12 +1813,13 @@
mQcConstants.MAX_JOB_COUNT_WORKING = 4000;
mQcConstants.MAX_JOB_COUNT_FREQUENT = 3000;
mQcConstants.MAX_JOB_COUNT_RARE = 2000;
- mQcConstants.MAX_JOB_COUNT_PER_ALLOWED_TIME = 500;
+ mQcConstants.RATE_LIMITING_WINDOW_MS = 15 * MINUTE_IN_MILLIS;
+ mQcConstants.MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW = 500;
mQcConstants.MAX_SESSION_COUNT_ACTIVE = 500;
mQcConstants.MAX_SESSION_COUNT_WORKING = 400;
mQcConstants.MAX_SESSION_COUNT_FREQUENT = 300;
mQcConstants.MAX_SESSION_COUNT_RARE = 200;
- mQcConstants.MAX_SESSION_COUNT_PER_ALLOWED_TIME = 50;
+ mQcConstants.MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW = 50;
mQcConstants.TIMING_SESSION_COALESCING_DURATION_MS = 10 * SECOND_IN_MILLIS;
mQcConstants.updateConstants();
@@ -1750,12 +1832,13 @@
mQuotaController.getBucketWindowSizes()[FREQUENT_INDEX]);
assertEquals(60 * MINUTE_IN_MILLIS, mQuotaController.getBucketWindowSizes()[RARE_INDEX]);
assertEquals(3 * HOUR_IN_MILLIS, mQuotaController.getMaxExecutionTimeMs());
- assertEquals(500, mQuotaController.getMaxJobCountPerAllowedTime());
+ assertEquals(15 * MINUTE_IN_MILLIS, mQuotaController.getRateLimitingWindowMs());
+ assertEquals(500, mQuotaController.getMaxJobCountPerRateLimitingWindow());
assertEquals(5000, mQuotaController.getBucketMaxJobCounts()[ACTIVE_INDEX]);
assertEquals(4000, mQuotaController.getBucketMaxJobCounts()[WORKING_INDEX]);
assertEquals(3000, mQuotaController.getBucketMaxJobCounts()[FREQUENT_INDEX]);
assertEquals(2000, mQuotaController.getBucketMaxJobCounts()[RARE_INDEX]);
- assertEquals(50, mQuotaController.getMaxSessionCountPerAllowedTime());
+ assertEquals(50, mQuotaController.getMaxSessionCountPerRateLimitingWindow());
assertEquals(500, mQuotaController.getBucketMaxSessionCounts()[ACTIVE_INDEX]);
assertEquals(400, mQuotaController.getBucketMaxSessionCounts()[WORKING_INDEX]);
assertEquals(300, mQuotaController.getBucketMaxSessionCounts()[FREQUENT_INDEX]);
@@ -1778,12 +1861,13 @@
mQcConstants.MAX_JOB_COUNT_WORKING = 1;
mQcConstants.MAX_JOB_COUNT_FREQUENT = 1;
mQcConstants.MAX_JOB_COUNT_RARE = 1;
- mQcConstants.MAX_JOB_COUNT_PER_ALLOWED_TIME = 0;
+ mQcConstants.RATE_LIMITING_WINDOW_MS = 15 * SECOND_IN_MILLIS;
+ mQcConstants.MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW = 0;
mQcConstants.MAX_SESSION_COUNT_ACTIVE = -1;
- mQcConstants.MAX_SESSION_COUNT_WORKING = 1;
- mQcConstants.MAX_SESSION_COUNT_FREQUENT = 2;
- mQcConstants.MAX_SESSION_COUNT_RARE = 1;
- mQcConstants.MAX_SESSION_COUNT_PER_ALLOWED_TIME = 0;
+ mQcConstants.MAX_SESSION_COUNT_WORKING = 0;
+ mQcConstants.MAX_SESSION_COUNT_FREQUENT = -3;
+ mQcConstants.MAX_SESSION_COUNT_RARE = 0;
+ mQcConstants.MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW = 0;
mQcConstants.TIMING_SESSION_COALESCING_DURATION_MS = -1;
mQcConstants.updateConstants();
@@ -1795,16 +1879,17 @@
assertEquals(MINUTE_IN_MILLIS, mQuotaController.getBucketWindowSizes()[FREQUENT_INDEX]);
assertEquals(MINUTE_IN_MILLIS, mQuotaController.getBucketWindowSizes()[RARE_INDEX]);
assertEquals(HOUR_IN_MILLIS, mQuotaController.getMaxExecutionTimeMs());
- assertEquals(10, mQuotaController.getMaxJobCountPerAllowedTime());
- assertEquals(100, mQuotaController.getBucketMaxJobCounts()[ACTIVE_INDEX]);
- assertEquals(100, mQuotaController.getBucketMaxJobCounts()[WORKING_INDEX]);
- assertEquals(100, mQuotaController.getBucketMaxJobCounts()[FREQUENT_INDEX]);
- assertEquals(100, mQuotaController.getBucketMaxJobCounts()[RARE_INDEX]);
- assertEquals(10, mQuotaController.getMaxSessionCountPerAllowedTime());
- assertEquals(3, mQuotaController.getBucketMaxSessionCounts()[ACTIVE_INDEX]);
- assertEquals(3, mQuotaController.getBucketMaxSessionCounts()[WORKING_INDEX]);
- assertEquals(3, mQuotaController.getBucketMaxSessionCounts()[FREQUENT_INDEX]);
- assertEquals(3, mQuotaController.getBucketMaxSessionCounts()[RARE_INDEX]);
+ assertEquals(30 * SECOND_IN_MILLIS, mQuotaController.getRateLimitingWindowMs());
+ assertEquals(10, mQuotaController.getMaxJobCountPerRateLimitingWindow());
+ assertEquals(10, mQuotaController.getBucketMaxJobCounts()[ACTIVE_INDEX]);
+ assertEquals(10, mQuotaController.getBucketMaxJobCounts()[WORKING_INDEX]);
+ assertEquals(10, mQuotaController.getBucketMaxJobCounts()[FREQUENT_INDEX]);
+ assertEquals(10, mQuotaController.getBucketMaxJobCounts()[RARE_INDEX]);
+ assertEquals(10, mQuotaController.getMaxSessionCountPerRateLimitingWindow());
+ assertEquals(1, mQuotaController.getBucketMaxSessionCounts()[ACTIVE_INDEX]);
+ assertEquals(1, mQuotaController.getBucketMaxSessionCounts()[WORKING_INDEX]);
+ assertEquals(1, mQuotaController.getBucketMaxSessionCounts()[FREQUENT_INDEX]);
+ assertEquals(1, mQuotaController.getBucketMaxSessionCounts()[RARE_INDEX]);
assertEquals(0, mQuotaController.getTimingSessionCoalescingDurationMs());
// Test larger than a day. Controller should cap at one day.
@@ -1815,6 +1900,7 @@
mQcConstants.WINDOW_SIZE_FREQUENT_MS = 25 * HOUR_IN_MILLIS;
mQcConstants.WINDOW_SIZE_RARE_MS = 25 * HOUR_IN_MILLIS;
mQcConstants.MAX_EXECUTION_TIME_MS = 25 * HOUR_IN_MILLIS;
+ mQcConstants.RATE_LIMITING_WINDOW_MS = 25 * HOUR_IN_MILLIS;
mQcConstants.TIMING_SESSION_COALESCING_DURATION_MS = 25 * HOUR_IN_MILLIS;
mQcConstants.updateConstants();
@@ -1826,6 +1912,7 @@
assertEquals(24 * HOUR_IN_MILLIS, mQuotaController.getBucketWindowSizes()[FREQUENT_INDEX]);
assertEquals(24 * HOUR_IN_MILLIS, mQuotaController.getBucketWindowSizes()[RARE_INDEX]);
assertEquals(24 * HOUR_IN_MILLIS, mQuotaController.getMaxExecutionTimeMs());
+ assertEquals(24 * HOUR_IN_MILLIS, mQuotaController.getRateLimitingWindowMs());
assertEquals(15 * MINUTE_IN_MILLIS,
mQuotaController.getTimingSessionCoalescingDurationMs());
}
@@ -2126,7 +2213,7 @@
assertNull(mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
ExecutionStats stats = mQuotaController.getExecutionStatsLocked(SOURCE_USER_ID,
SOURCE_PACKAGE, standbyBucket);
- assertEquals(0, stats.jobCountInAllowedTime);
+ assertEquals(0, stats.jobCountInRateLimitingWindow);
setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
mQuotaController.prepareForExecutionLocked(jobFg1);
@@ -2138,7 +2225,7 @@
mQuotaController.maybeStopTrackingJobLocked(jobFg2, null, false);
assertNull(mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
- assertEquals(0, stats.jobCountInAllowedTime);
+ assertEquals(0, stats.jobCountInRateLimitingWindow);
}
/**
@@ -2154,7 +2241,7 @@
ExecutionStats stats = mQuotaController.getExecutionStatsLocked(SOURCE_USER_ID,
SOURCE_PACKAGE, standbyBucket);
- assertEquals(0, stats.jobCountInAllowedTime);
+ assertEquals(0, stats.jobCountInRateLimitingWindow);
setProcessState(ActivityManager.PROCESS_STATE_TOP_SLEEPING);
mQuotaController.prepareForExecutionLocked(jobBg1);
@@ -2165,7 +2252,7 @@
advanceElapsedClock(10 * SECOND_IN_MILLIS);
mQuotaController.maybeStopTrackingJobLocked(jobBg2, null, false);
- assertEquals(2, stats.jobCountInAllowedTime);
+ assertEquals(2, stats.jobCountInRateLimitingWindow);
}
/**
@@ -2421,10 +2508,10 @@
/**
* Tests that the start alarm is properly scheduled when a job has been throttled due to the job
- * count quota.
+ * count rate limiting.
*/
@Test
- public void testStartAlarmScheduled_JobCount_AllowedTime() {
+ public void testStartAlarmScheduled_JobCount_RateLimitingWindow() {
// saveTimingSession calls maybeScheduleCleanupAlarmLocked which interferes with these tests
// because it schedules an alarm too. Prevent it from doing so.
spyOn(mQuotaController);
@@ -2432,7 +2519,7 @@
// Essentially disable session throttling.
mQcConstants.MAX_SESSION_COUNT_WORKING =
- mQcConstants.MAX_SESSION_COUNT_PER_ALLOWED_TIME = Integer.MAX_VALUE;
+ mQcConstants.MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW = Integer.MAX_VALUE;
mQcConstants.updateConstants();
final int standbyBucket = WORKING_INDEX;
@@ -2444,7 +2531,7 @@
verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
// Ran jobs up to the job limit. All of them should be allowed to run.
- for (int i = 0; i < mQcConstants.MAX_JOB_COUNT_PER_ALLOWED_TIME; ++i) {
+ for (int i = 0; i < mQcConstants.MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW; ++i) {
JobStatus job = createJobStatus("testStartAlarmScheduled_JobCount_AllowedTime", i);
setStandbyBucket(WORKING_INDEX, job);
mQuotaController.maybeStartTrackingJobLocked(job, null);
@@ -2466,18 +2553,17 @@
ExecutionStats stats = mQuotaController.getExecutionStatsLocked(SOURCE_USER_ID,
SOURCE_PACKAGE, standbyBucket);
- final long expectedWorkingAlarmTime =
- stats.jobCountExpirationTimeElapsed + mQcConstants.ALLOWED_TIME_PER_PERIOD_MS;
+ final long expectedWorkingAlarmTime = stats.jobRateLimitExpirationTimeElapsed;
verify(mAlarmManager, times(1))
.set(anyInt(), eq(expectedWorkingAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
}
/**
- * Tests that the start alarm is properly scheduled when a job has been throttled due to the job
- * count quota.
+ * Tests that the start alarm is properly scheduled when a job has been throttled due to the
+ * session count rate limiting.
*/
@Test
- public void testStartAlarmScheduled_TimingSessionCount_AllowedTime() {
+ public void testStartAlarmScheduled_TimingSessionCount_RateLimitingWindow() {
// saveTimingSession calls maybeScheduleCleanupAlarmLocked which interferes with these tests
// because it schedules an alarm too. Prevent it from doing so.
spyOn(mQuotaController);
@@ -2485,10 +2571,10 @@
// Essentially disable job count throttling.
mQcConstants.MAX_JOB_COUNT_FREQUENT =
- mQcConstants.MAX_JOB_COUNT_PER_ALLOWED_TIME = Integer.MAX_VALUE;
- // Make sure throttling is because of COUNT_PER_ALLOWED_TIME.
+ mQcConstants.MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW = Integer.MAX_VALUE;
+ // Make sure throttling is because of COUNT_PER_RATE_LIMITING_WINDOW.
mQcConstants.MAX_SESSION_COUNT_FREQUENT =
- mQcConstants.MAX_SESSION_COUNT_PER_ALLOWED_TIME + 1;
+ mQcConstants.MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW + 1;
mQcConstants.updateConstants();
final int standbyBucket = FREQUENT_INDEX;
@@ -2500,7 +2586,7 @@
verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
// Ran jobs up to the job limit. All of them should be allowed to run.
- for (int i = 0; i < mQcConstants.MAX_SESSION_COUNT_PER_ALLOWED_TIME; ++i) {
+ for (int i = 0; i < mQcConstants.MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW; ++i) {
JobStatus job = createJobStatus(
"testStartAlarmScheduled_TimingSessionCount_AllowedTime", i);
setStandbyBucket(FREQUENT_INDEX, job);
@@ -2515,7 +2601,7 @@
// Start alarm shouldn't have been scheduled since the app was in quota up until this point.
verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
- // The app is now out of job count quota
+ // The app is now out of session count quota
JobStatus throttledJob = createJobStatus(
"testStartAlarmScheduled_TimingSessionCount_AllowedTime", 42);
mQuotaController.maybeStartTrackingJobLocked(throttledJob, null);
@@ -2523,8 +2609,7 @@
ExecutionStats stats = mQuotaController.getExecutionStatsLocked(SOURCE_USER_ID,
SOURCE_PACKAGE, standbyBucket);
- final long expectedWorkingAlarmTime =
- stats.sessionCountExpirationTimeElapsed + mQcConstants.ALLOWED_TIME_PER_PERIOD_MS;
+ final long expectedWorkingAlarmTime = stats.sessionRateLimitExpirationTimeElapsed;
verify(mAlarmManager, times(1))
.set(anyInt(), eq(expectedWorkingAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
}
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/telephony/java/android/telephony/ServiceState.java b/telephony/java/android/telephony/ServiceState.java
index 984d8f7..5d39a2c 100644
--- a/telephony/java/android/telephony/ServiceState.java
+++ b/telephony/java/android/telephony/ServiceState.java
@@ -341,6 +341,7 @@
private String mOperatorAlphaLongRaw;
private String mOperatorAlphaShortRaw;
+ private boolean mIsIwlanPreferred;
/**
* get String description of roaming type
@@ -427,6 +428,7 @@
mNrFrequencyRange = s.mNrFrequencyRange;
mOperatorAlphaLongRaw = s.mOperatorAlphaLongRaw;
mOperatorAlphaShortRaw = s.mOperatorAlphaShortRaw;
+ mIsIwlanPreferred = s.mIsIwlanPreferred;
}
/**
@@ -463,6 +465,7 @@
mNrFrequencyRange = in.readInt();
mOperatorAlphaLongRaw = in.readString();
mOperatorAlphaShortRaw = in.readString();
+ mIsIwlanPreferred = in.readBoolean();
}
public void writeToParcel(Parcel out, int flags) {
@@ -492,6 +495,7 @@
out.writeInt(mNrFrequencyRange);
out.writeString(mOperatorAlphaLongRaw);
out.writeString(mOperatorAlphaShortRaw);
+ out.writeBoolean(mIsIwlanPreferred);
}
public int describeContents() {
@@ -853,7 +857,8 @@
mNetworkRegistrationInfos,
mNrFrequencyRange,
mOperatorAlphaLongRaw,
- mOperatorAlphaShortRaw);
+ mOperatorAlphaShortRaw,
+ mIsIwlanPreferred);
}
}
@@ -885,7 +890,8 @@
&& equalsHandlesNulls(mOperatorAlphaShortRaw, s.mOperatorAlphaShortRaw)
&& mNetworkRegistrationInfos.size() == s.mNetworkRegistrationInfos.size()
&& mNetworkRegistrationInfos.containsAll(s.mNetworkRegistrationInfos)
- && mNrFrequencyRange == s.mNrFrequencyRange;
+ && mNrFrequencyRange == s.mNrFrequencyRange
+ && mIsIwlanPreferred == s.mIsIwlanPreferred;
}
}
@@ -1043,6 +1049,7 @@
.append(", mNrFrequencyRange=").append(mNrFrequencyRange)
.append(", mOperatorAlphaLongRaw=").append(mOperatorAlphaLongRaw)
.append(", mOperatorAlphaShortRaw=").append(mOperatorAlphaShortRaw)
+ .append(", mIsIwlanPreferred=").append(mIsIwlanPreferred)
.append("}").toString();
}
}
@@ -1085,6 +1092,7 @@
}
mOperatorAlphaLongRaw = null;
mOperatorAlphaShortRaw = null;
+ mIsIwlanPreferred = false;
}
public void setStateOutOfService() {
@@ -1459,20 +1467,9 @@
/** @hide */
@UnsupportedAppUsage
public int getRilDataRadioTechnology() {
- NetworkRegistrationInfo wwanRegInfo = getNetworkRegistrationInfo(
- NetworkRegistrationInfo.DOMAIN_PS, AccessNetworkConstants.TRANSPORT_TYPE_WWAN);
- NetworkRegistrationInfo wlanRegInfo = getNetworkRegistrationInfo(
- NetworkRegistrationInfo.DOMAIN_PS, AccessNetworkConstants.TRANSPORT_TYPE_WLAN);
- if (wlanRegInfo != null
- && wlanRegInfo.getAccessNetworkTechnology() == TelephonyManager.NETWORK_TYPE_IWLAN
- && wlanRegInfo.getRegistrationState()
- == NetworkRegistrationInfo.REGISTRATION_STATE_HOME) {
- return RIL_RADIO_TECHNOLOGY_IWLAN;
- } else if (wwanRegInfo != null) {
- return networkTypeToRilRadioTechnology(wwanRegInfo.getAccessNetworkTechnology());
- }
- return RIL_RADIO_TECHNOLOGY_UNKNOWN;
+ return networkTypeToRilRadioTechnology(getDataNetworkType());
}
+
/**
* @hide
* @Deprecated to be removed Q3 2013 use {@link #getRilDataRadioTechnology} or
@@ -1608,26 +1605,40 @@
}
}
- /** @hide */
+ /**
+ * Get current data network type.
+ *
+ * Note that for IWLAN AP-assisted mode device, which is reporting both camped access networks
+ * (cellular RAT and IWLAN)at the same time, this API is simulating the old legacy mode device
+ * behavior,
+ *
+ * @return Current data network type
+ * @hide
+ */
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
public @TelephonyManager.NetworkType int getDataNetworkType() {
- final NetworkRegistrationInfo iwlanRegState = getNetworkRegistrationInfo(
+ final NetworkRegistrationInfo iwlanRegInfo = getNetworkRegistrationInfo(
NetworkRegistrationInfo.DOMAIN_PS, AccessNetworkConstants.TRANSPORT_TYPE_WLAN);
- if (iwlanRegState != null && iwlanRegState.getRegistrationState()
- == NetworkRegistrationInfo.REGISTRATION_STATE_HOME) {
- // If the device is on IWLAN, return IWLAN as the network type. This is to simulate the
- // behavior of legacy mode device. In the future caller should use
- // requestNetworkRegistrationInfo() to retrieve the actual data network type on cellular
- // or on IWLAN.
- return iwlanRegState.getAccessNetworkTechnology();
+ final NetworkRegistrationInfo wwanRegInfo = getNetworkRegistrationInfo(
+ NetworkRegistrationInfo.DOMAIN_PS, AccessNetworkConstants.TRANSPORT_TYPE_WWAN);
+
+ // For legacy mode device, or AP-assisted mode device but IWLAN is out of service, use
+ // the RAT from cellular.
+ if (iwlanRegInfo == null || !iwlanRegInfo.isInService()) {
+ return (wwanRegInfo != null) ? wwanRegInfo.getAccessNetworkTechnology()
+ : TelephonyManager.NETWORK_TYPE_UNKNOWN;
}
- final NetworkRegistrationInfo regState = getNetworkRegistrationInfo(
- NetworkRegistrationInfo.DOMAIN_PS, AccessNetworkConstants.TRANSPORT_TYPE_WWAN);
- if (regState != null) {
- return regState.getAccessNetworkTechnology();
+ // At this point, it must be an AP-assisted mode device and IWLAN is in service. We should
+ // use the RAT from IWLAN service is cellular is out of service, or when both are in service
+ // and any APN type of data is preferred on IWLAN.
+ if (!wwanRegInfo.isInService() || mIsIwlanPreferred) {
+ return iwlanRegInfo.getAccessNetworkTechnology();
}
- return TelephonyManager.NETWORK_TYPE_UNKNOWN;
+
+ // If both cellular and IWLAN are in service, but no APN is preferred on IWLAN, still use
+ // the RAT from cellular.
+ return wwanRegInfo.getAccessNetworkTechnology();
}
/** @hide */
@@ -1976,4 +1987,14 @@
public String getOperatorAlphaShortRaw() {
return mOperatorAlphaShortRaw;
}
+
+ /**
+ * Set to {@code true} if any data network is preferred on IWLAN.
+ *
+ * @param isIwlanPreferred {@code true} if IWLAN is preferred.
+ * @hide
+ */
+ public void setIwlanPreferred(boolean isIwlanPreferred) {
+ mIsIwlanPreferred = isIwlanPreferred;
+ }
}
diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java
index addd9e0..e010d28 100644
--- a/telephony/java/android/telephony/SubscriptionManager.java
+++ b/telephony/java/android/telephony/SubscriptionManager.java
@@ -775,6 +775,14 @@
public static final int PROFILE_CLASS_DEFAULT = PROFILE_CLASS_UNSET;
/**
+ * TelephonyProvider column name IMSI (International Mobile Subscriber Identity).
+ * <P>Type: TEXT </P>
+ * @hide
+ */
+ //TODO: add @SystemApi
+ public static final String IMSI = "imsi";
+
+ /**
* Broadcast Action: The user has changed one of the default subs related to
* data, phone calls, or sms</p>
*
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");
+ }
}
diff --git a/tests/net/java/android/net/NetworkStatsTest.java b/tests/net/java/android/net/NetworkStatsTest.java
index b5b0384..c16a0f4 100644
--- a/tests/net/java/android/net/NetworkStatsTest.java
+++ b/tests/net/java/android/net/NetworkStatsTest.java
@@ -569,7 +569,7 @@
.addValues(underlyingIface, tunUid, SET_FOREGROUND, TAG_NONE, METERED_NO, ROAMING_NO,
DEFAULT_NETWORK_NO, 0L, 0L, 0L, 0L, 0L);
- assertTrue(delta.toString(), delta.migrateTun(tunUid, tunIface, underlyingIface));
+ delta.migrateTun(tunUid, tunIface, new String[] {underlyingIface});
assertEquals(20, delta.size());
// tunIface and TEST_IFACE entries are not changed.
@@ -650,7 +650,7 @@
.addValues(underlyingIface, tunUid, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
DEFAULT_NETWORK_NO, 75500L, 37L, 130000L, 70L, 0L);
- assertTrue(delta.migrateTun(tunUid, tunIface, underlyingIface));
+ delta.migrateTun(tunUid, tunIface, new String[]{underlyingIface});
assertEquals(9, delta.size());
// tunIface entries should not be changed.
@@ -813,6 +813,37 @@
}
@Test
+ public void testFilterDebugEntries() {
+ NetworkStats.Entry entry1 = new NetworkStats.Entry(
+ "test1", 10100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+ DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L);
+
+ NetworkStats.Entry entry2 = new NetworkStats.Entry(
+ "test2", 10101, SET_DBG_VPN_IN, TAG_NONE, METERED_NO, ROAMING_NO,
+ DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L);
+
+ NetworkStats.Entry entry3 = new NetworkStats.Entry(
+ "test2", 10101, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+ DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L);
+
+ NetworkStats.Entry entry4 = new NetworkStats.Entry(
+ "test2", 10101, SET_DBG_VPN_OUT, TAG_NONE, METERED_NO, ROAMING_NO,
+ DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L);
+
+ NetworkStats stats = new NetworkStats(TEST_START, 4)
+ .addValues(entry1)
+ .addValues(entry2)
+ .addValues(entry3)
+ .addValues(entry4);
+
+ stats.filterDebugEntries();
+
+ assertEquals(2, stats.size());
+ assertEquals(entry1, stats.getValues(0, null));
+ assertEquals(entry3, stats.getValues(1, null));
+ }
+
+ @Test
public void testApply464xlatAdjustments() {
final String v4Iface = "v4-wlan0";
final String baseIface = "wlan0";
diff --git a/tests/net/java/com/android/server/connectivity/VpnTest.java b/tests/net/java/com/android/server/connectivity/VpnTest.java
index 2cae250..ce50bef 100644
--- a/tests/net/java/com/android/server/connectivity/VpnTest.java
+++ b/tests/net/java/com/android/server/connectivity/VpnTest.java
@@ -727,94 +727,4 @@
"::/1", "8000::/2", "c000::/3", "e000::/4", "f000::/5", "f800::/6",
"fe00::/8", "2605:ef80:e:af1d::/64");
}
-
- @Test
- public void testProvidesRoutesToMostDestinations() {
- final LinkProperties lp = new LinkProperties();
-
- // Default route provides routes to all IPv4 destinations.
- lp.addRoute(new RouteInfo(new IpPrefix("0.0.0.0/0")));
- assertTrue(Vpn.providesRoutesToMostDestinations(lp));
-
- // Empty LP provides routes to no destination
- lp.clear();
- assertFalse(Vpn.providesRoutesToMostDestinations(lp));
-
- // All IPv4 routes except for local networks. This is the case most relevant
- // to this function. It provides routes to almost the entire space.
- // (clone the stream so that we can reuse it later)
- publicIpV4Routes().forEach(s -> lp.addRoute(new RouteInfo(new IpPrefix(s))));
- assertTrue(Vpn.providesRoutesToMostDestinations(lp));
-
- // Removing a 16-bit prefix, which is 65536 addresses. This is still enough to
- // provide routes to "most" destinations.
- lp.removeRoute(new RouteInfo(new IpPrefix("192.169.0.0/16")));
- assertTrue(Vpn.providesRoutesToMostDestinations(lp));
-
- // Remove the /2 route, which represent a quarter of the available routing space.
- // This LP does not provides routes to "most" destinations any more.
- lp.removeRoute(new RouteInfo(new IpPrefix("64.0.0.0/2")));
- assertFalse(Vpn.providesRoutesToMostDestinations(lp));
-
- lp.clear();
- publicIpV6Routes().forEach(s -> lp.addRoute(new RouteInfo(new IpPrefix(s))));
- assertTrue(Vpn.providesRoutesToMostDestinations(lp));
-
- lp.removeRoute(new RouteInfo(new IpPrefix("::/1")));
- assertFalse(Vpn.providesRoutesToMostDestinations(lp));
-
- // V6 does not provide sufficient coverage but v4 does
- publicIpV4Routes().forEach(s -> lp.addRoute(new RouteInfo(new IpPrefix(s))));
- assertTrue(Vpn.providesRoutesToMostDestinations(lp));
-
- // V4 still does
- lp.removeRoute(new RouteInfo(new IpPrefix("192.169.0.0/16")));
- assertTrue(Vpn.providesRoutesToMostDestinations(lp));
-
- // V4 does not any more
- lp.removeRoute(new RouteInfo(new IpPrefix("64.0.0.0/2")));
- assertFalse(Vpn.providesRoutesToMostDestinations(lp));
-
- // V4 does not, but V6 has sufficient coverage again
- lp.addRoute(new RouteInfo(new IpPrefix("::/1")));
- assertTrue(Vpn.providesRoutesToMostDestinations(lp));
-
- lp.clear();
- // V4-unreachable route should not be treated as sufficient coverage
- lp.addRoute(new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), RTN_UNREACHABLE));
- assertFalse(Vpn.providesRoutesToMostDestinations(lp));
-
- lp.clear();
- // V6-unreachable route should not be treated as sufficient coverage
- lp.addRoute(new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), RTN_UNREACHABLE));
- assertFalse(Vpn.providesRoutesToMostDestinations(lp));
- }
-
- @Test
- public void testDoesNotLockUpWithTooManyRoutes() {
- final LinkProperties lp = new LinkProperties();
- final byte[] ad = new byte[4];
- // Actually evaluating this many routes under 1500ms is impossible on
- // current hardware and for some time, as the algorithm is O(n²).
- // Make sure the system has a safeguard against this and does not
- // lock up.
- final int MAX_ROUTES = 4000;
- final long MAX_ALLOWED_TIME_MS = 1500;
- for (int i = 0; i < MAX_ROUTES; ++i) {
- ad[0] = (byte)((i >> 24) & 0xFF);
- ad[1] = (byte)((i >> 16) & 0xFF);
- ad[2] = (byte)((i >> 8) & 0xFF);
- ad[3] = (byte)(i & 0xFF);
- try {
- lp.addRoute(new RouteInfo(new IpPrefix(Inet4Address.getByAddress(ad), 32)));
- } catch (UnknownHostException e) {
- // UnknownHostException is only thrown for an address of illegal length,
- // which can't happen in the case above.
- }
- }
- final long start = SystemClock.currentThreadTimeMillis();
- assertTrue(Vpn.providesRoutesToMostDestinations(lp));
- final long end = SystemClock.currentThreadTimeMillis();
- assertTrue(end - start < MAX_ALLOWED_TIME_MS);
- }
}
diff --git a/tests/net/java/com/android/server/net/NetworkStatsAccessTest.java b/tests/net/java/com/android/server/net/NetworkStatsAccessTest.java
index 6e725dd..858358c 100644
--- a/tests/net/java/com/android/server/net/NetworkStatsAccessTest.java
+++ b/tests/net/java/com/android/server/net/NetworkStatsAccessTest.java
@@ -161,7 +161,7 @@
}
private void setHasCarrierPrivileges(boolean hasPrivileges) {
- when(mTm.checkCarrierPrivilegesForPackage(TEST_PKG)).thenReturn(
+ when(mTm.checkCarrierPrivilegesForPackageAnyPhone(TEST_PKG)).thenReturn(
hasPrivileges ? TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS
: TelephonyManager.CARRIER_PRIVILEGE_STATUS_NO_ACCESS);
}
diff --git a/tests/net/java/com/android/server/net/NetworkStatsServiceTest.java b/tests/net/java/com/android/server/net/NetworkStatsServiceTest.java
index bce526d..d9f2c20 100644
--- a/tests/net/java/com/android/server/net/NetworkStatsServiceTest.java
+++ b/tests/net/java/com/android/server/net/NetworkStatsServiceTest.java
@@ -57,11 +57,11 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
-import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -216,11 +216,16 @@
expectNetworkStatsUidDetail(buildEmptyStats());
expectSystemReady();
+ assertNull(mService.getTunAdjustedStats());
mService.systemReady();
+ // Verify that system ready fetches realtime stats and initializes tun adjusted stats.
+ verify(mNetManager).getNetworkStatsUidDetail(UID_ALL, INTERFACES_ALL);
+ assertNotNull("failed to initialize TUN adjusted stats", mService.getTunAdjustedStats());
+ assertEquals(0, mService.getTunAdjustedStats().size());
+
mSession = mService.openSession();
assertNotNull("openSession() failed", mSession);
-
// catch INetworkManagementEventObserver during systemReady()
ArgumentCaptor<INetworkManagementEventObserver> networkObserver =
ArgumentCaptor.forClass(INetworkManagementEventObserver.class);
@@ -733,11 +738,13 @@
NetworkStats stats = mService.getDetailedUidStats(ifaceFilter);
- verify(mNetManager, times(1)).getNetworkStatsUidDetail(eq(UID_ALL), argThat(ifaces ->
- ifaces != null && ifaces.length == 2
- && ArrayUtils.contains(ifaces, TEST_IFACE)
- && ArrayUtils.contains(ifaces, stackedIface)));
-
+ // mNetManager#getNetworkStatsUidDetail(UID_ALL, INTERFACES_ALL) has following invocations:
+ // 1) NetworkStatsService#systemReady from #setUp.
+ // 2) mService#forceUpdateIfaces in the test above.
+ // 3) Finally, mService#getDetailedUidStats.
+ verify(mNetManager, times(3)).getNetworkStatsUidDetail(UID_ALL, INTERFACES_ALL);
+ assertTrue(ArrayUtils.contains(stats.getUniqueIfaces(), TEST_IFACE));
+ assertTrue(ArrayUtils.contains(stats.getUniqueIfaces(), stackedIface));
assertEquals(2, stats.size());
assertEquals(uidStats, stats.getValues(0, null));
assertEquals(tetheredStats1, stats.getValues(1, null));
@@ -923,11 +930,11 @@
}
@Test
- public void vpnWithOneUnderlyingIface() throws Exception {
+ public void vpnRewriteTrafficThroughItself() throws Exception {
// WiFi network is connected and VPN is using WiFi (which has TEST_IFACE).
expectDefaultSettings();
NetworkState[] networkStates = new NetworkState[] {buildWifiState(), buildVpnState()};
- VpnInfo[] vpnInfos = new VpnInfo[] {createVpnInfo(TEST_IFACE)};
+ VpnInfo[] vpnInfos = new VpnInfo[] {createVpnInfo(new String[] {TEST_IFACE})};
expectNetworkStatsUidDetail(buildEmptyStats());
expectBandwidthControlCheck();
@@ -938,23 +945,133 @@
getActiveIface(networkStates));
// create some traffic (assume 10 bytes of MTU for VPN interface and 1 byte encryption
// overhead per packet):
- // 1000 bytes (100 packets) were sent/received by UID_RED over VPN.
- // 500 bytes (50 packets) were sent/received by UID_BLUE over VPN.
- // VPN sent/received 1650 bytes (150 packets) over WiFi.
- // Of 1650 bytes over WiFi, expect 1000 bytes attributed to UID_RED, 500 bytes attributed to
- // UID_BLUE, and 150 bytes attributed to UID_VPN for both rx/tx traffic.
+ //
+ // 1000 bytes (100 packets) were sent, and 2000 bytes (200 packets) were received by UID_RED
+ // over VPN.
+ // 500 bytes (50 packets) were sent, and 1000 bytes (100 packets) were received by UID_BLUE
+ // over VPN.
+ //
+ // VPN UID rewrites packets read from TUN back to TUN, plus some of its own traffic
+ // (100 bytes).
incrementCurrentTime(HOUR_IN_MILLIS);
- expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 3)
- .addValues(TUN_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 1000L, 100L, 1000L, 100L, 1L)
- .addValues(TUN_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 500L, 50L, 500L, 50L, 1L)
- .addValues(
- TEST_IFACE, UID_VPN, SET_DEFAULT, TAG_NONE, 1650L, 150L, 1650L, 150L, 2L));
+ expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 5)
+ .addValues(TUN_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 2000L, 200L, 1000L, 100L, 1L)
+ .addValues(TUN_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 1000L, 100L, 500L, 50L, 1L)
+ // VPN rewrites all the packets read from TUN + 100 additional bytes of VPN's
+ // own traffic.
+ .addValues(TUN_IFACE, UID_VPN, SET_DEFAULT, TAG_NONE, 0L, 0L, 1600L, 160L, 2L)
+ // VPN sent 1760 bytes over WiFi in foreground (SET_FOREGROUND) i.e. 1600
+ // bytes (160 packets) + 1 byte/packet overhead (=160 bytes).
+ .addValues(TEST_IFACE, UID_VPN, SET_FOREGROUND, TAG_NONE, 0L, 0L, 1760L, 176L, 1L)
+ // VPN received 3300 bytes over WiFi in background (SET_DEFAULT) i.e. 3000 bytes
+ // (300 packets) + 1 byte/packet encryption overhead (=300 bytes).
+ .addValues(TEST_IFACE, UID_VPN, SET_DEFAULT, TAG_NONE, 3300L, 300L, 0L, 0L, 1L));
forcePollAndWaitForIdle();
- assertUidTotal(sTemplateWifi, UID_RED, 1000L, 100L, 1000L, 100L, 1);
- assertUidTotal(sTemplateWifi, UID_BLUE, 500L, 50L, 500L, 50L, 1);
- assertUidTotal(sTemplateWifi, UID_VPN, 150L, 0L, 150L, 0L, 2);
+ // Verify increased TUN usage by UID_VPN does not get attributed to other apps.
+ NetworkStats tunStats =
+ mService.getDetailedUidStats(new String[] {TUN_IFACE});
+ assertValues(
+ tunStats, TUN_IFACE, UID_RED, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL,
+ DEFAULT_NETWORK_ALL, 2000L, 200L, 1000L, 100L, 1);
+ assertValues(
+ tunStats, TUN_IFACE, UID_BLUE, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL,
+ DEFAULT_NETWORK_ALL, 1000L, 100L, 500L, 50L, 1);
+ assertValues(
+ tunStats, TUN_IFACE, UID_VPN, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL,
+ DEFAULT_NETWORK_ALL, 0L, 0L, 1600L, 160L, 2);
+
+ // Verify correct attribution over WiFi.
+ assertUidTotal(sTemplateWifi, UID_RED, 2000L, 200L, 1000L, 100L, 1);
+ assertUidTotal(sTemplateWifi, UID_BLUE, 1000L, 100L, 500L, 50L, 1);
+ assertUidTotal(sTemplateWifi, UID_VPN, 300L, 0L, 260L, 26L, 2);
+ }
+
+ @Test
+ public void vpnWithOneUnderlyingIface() throws Exception {
+ // WiFi network is connected and VPN is using WiFi (which has TEST_IFACE).
+ expectDefaultSettings();
+ NetworkState[] networkStates = new NetworkState[] {buildWifiState(), buildVpnState()};
+ VpnInfo[] vpnInfos = new VpnInfo[] {createVpnInfo(new String[] {TEST_IFACE})};
+ expectNetworkStatsUidDetail(buildEmptyStats());
+ expectBandwidthControlCheck();
+
+ mService.forceUpdateIfaces(
+ new Network[] {WIFI_NETWORK, VPN_NETWORK},
+ vpnInfos,
+ networkStates,
+ getActiveIface(networkStates));
+ // create some traffic (assume 10 bytes of MTU for VPN interface and 1 byte encryption
+ // overhead per packet):
+ // 1000 bytes (100 packets) were sent, and 2000 bytes (200 packets) were received by UID_RED
+ // over VPN.
+ // 500 bytes (50 packets) were sent, and 1000 bytes (100 packets) were received by UID_BLUE
+ // over VPN.
+ // VPN sent 1650 bytes (150 packets), and received 3300 (300 packets) over WiFi.
+ // Of 1650 bytes sent over WiFi, expect 1000 bytes attributed to UID_RED, 500 bytes
+ // attributed to UID_BLUE, and 150 bytes attributed to UID_VPN.
+ // Of 3300 bytes received over WiFi, expect 2000 bytes attributed to UID_RED, 1000 bytes
+ // attributed to UID_BLUE, and 300 bytes attributed to UID_VPN.
+ incrementCurrentTime(HOUR_IN_MILLIS);
+ expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 3)
+ .addValues(TUN_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 2000L, 200L, 1000L, 100L, 1L)
+ .addValues(TUN_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 1000L, 100L, 500L, 50L, 1L)
+ // VPN received 3300 bytes over WiFi in background (SET_DEFAULT).
+ .addValues(TEST_IFACE, UID_VPN, SET_DEFAULT, TAG_NONE, 3300L, 300L, 0L, 0L, 1L)
+ // VPN sent 1650 bytes over WiFi in foreground (SET_FOREGROUND).
+ .addValues(TEST_IFACE, UID_VPN, SET_FOREGROUND, TAG_NONE, 0L, 0L, 1650L, 150L, 1L));
+
+ forcePollAndWaitForIdle();
+
+ assertUidTotal(sTemplateWifi, UID_RED, 2000L, 200L, 1000L, 100L, 1);
+ assertUidTotal(sTemplateWifi, UID_BLUE, 1000L, 100L, 500L, 50L, 1);
+ assertUidTotal(sTemplateWifi, UID_VPN, 300L, 0L, 150L, 0L, 2);
+ }
+
+ @Test
+ public void vpnWithOneUnderlyingIfaceAndOwnTraffic() throws Exception {
+ // WiFi network is connected and VPN is using WiFi (which has TEST_IFACE).
+ expectDefaultSettings();
+ NetworkState[] networkStates = new NetworkState[] {buildWifiState(), buildVpnState()};
+ VpnInfo[] vpnInfos = new VpnInfo[] {createVpnInfo(new String[] {TEST_IFACE})};
+ expectNetworkStatsUidDetail(buildEmptyStats());
+ expectBandwidthControlCheck();
+
+ mService.forceUpdateIfaces(
+ new Network[] {WIFI_NETWORK, VPN_NETWORK},
+ vpnInfos,
+ networkStates,
+ getActiveIface(networkStates));
+ // create some traffic (assume 10 bytes of MTU for VPN interface and 1 byte encryption
+ // overhead per packet):
+ // 1000 bytes (100 packets) were sent, and 2000 bytes (200 packets) were received by UID_RED
+ // over VPN.
+ // 500 bytes (50 packets) were sent, and 1000 bytes (100 packets) were received by UID_BLUE
+ // over VPN.
+ // Additionally, the VPN sends 6000 bytes (600 packets) of its own traffic into the tun
+ // interface (passing that traffic to the VPN endpoint), and receives 5000 bytes (500
+ // packets) from it. Including overhead that is 6600/5500 bytes.
+ // VPN sent 8250 bytes (750 packets), and received 8800 (800 packets) over WiFi.
+ // Of 8250 bytes sent over WiFi, expect 1000 bytes attributed to UID_RED, 500 bytes
+ // attributed to UID_BLUE, and 6750 bytes attributed to UID_VPN.
+ // Of 8800 bytes received over WiFi, expect 2000 bytes attributed to UID_RED, 1000 bytes
+ // attributed to UID_BLUE, and 5800 bytes attributed to UID_VPN.
+ incrementCurrentTime(HOUR_IN_MILLIS);
+ expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 3)
+ .addValues(TUN_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 2000L, 200L, 1000L, 100L, 1L)
+ .addValues(TUN_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 1000L, 100L, 500L, 50L, 1L)
+ .addValues(TUN_IFACE, UID_VPN, SET_DEFAULT, TAG_NONE, 5000L, 500L, 6000L, 600L, 1L)
+ // VPN received 8800 bytes over WiFi in background (SET_DEFAULT).
+ .addValues(TEST_IFACE, UID_VPN, SET_DEFAULT, TAG_NONE, 8800L, 800L, 0L, 0L, 1L)
+ // VPN sent 8250 bytes over WiFi in foreground (SET_FOREGROUND).
+ .addValues(TEST_IFACE, UID_VPN, SET_FOREGROUND, TAG_NONE, 0L, 0L, 8250L, 750L, 1L));
+
+ forcePollAndWaitForIdle();
+
+ assertUidTotal(sTemplateWifi, UID_RED, 2000L, 200L, 1000L, 100L, 1);
+ assertUidTotal(sTemplateWifi, UID_BLUE, 1000L, 100L, 500L, 50L, 1);
+ assertUidTotal(sTemplateWifi, UID_VPN, 5800L, 500L, 6750L, 600L, 2);
}
@Test
@@ -962,7 +1079,7 @@
// WiFi network is connected and VPN is using WiFi (which has TEST_IFACE).
expectDefaultSettings();
NetworkState[] networkStates = new NetworkState[] {buildWifiState(), buildVpnState()};
- VpnInfo[] vpnInfos = new VpnInfo[] {createVpnInfo(TEST_IFACE)};
+ VpnInfo[] vpnInfos = new VpnInfo[] {createVpnInfo(new String[] {TEST_IFACE})};
expectNetworkStatsUidDetail(buildEmptyStats());
expectBandwidthControlCheck();
@@ -993,6 +1110,136 @@
}
@Test
+ public void vpnWithTwoUnderlyingIfaces_packetDuplication() throws Exception {
+ // WiFi and Cell networks are connected and VPN is using WiFi (which has TEST_IFACE) and
+ // Cell (which has TEST_IFACE2) and has declared both of them in its underlying network set.
+ // Additionally, VPN is duplicating traffic across both WiFi and Cell.
+ expectDefaultSettings();
+ NetworkState[] networkStates =
+ new NetworkState[] {
+ buildWifiState(), buildMobile4gState(TEST_IFACE2), buildVpnState()
+ };
+ VpnInfo[] vpnInfos = new VpnInfo[] {createVpnInfo(new String[] {TEST_IFACE, TEST_IFACE2})};
+ expectNetworkStatsUidDetail(buildEmptyStats());
+ expectBandwidthControlCheck();
+
+ mService.forceUpdateIfaces(
+ new Network[] {WIFI_NETWORK, VPN_NETWORK},
+ vpnInfos,
+ networkStates,
+ getActiveIface(networkStates));
+ // create some traffic (assume 10 bytes of MTU for VPN interface and 1 byte encryption
+ // overhead per packet):
+ // 1000 bytes (100 packets) were sent/received by UID_RED and UID_BLUE over VPN.
+ // VPN sent/received 4400 bytes (400 packets) over both WiFi and Cell (8800 bytes in total).
+ // Of 8800 bytes over WiFi/Cell, expect:
+ // - 500 bytes rx/tx each over WiFi/Cell attributed to both UID_RED and UID_BLUE.
+ // - 1200 bytes rx/tx each over WiFi/Cell for VPN_UID.
+ incrementCurrentTime(HOUR_IN_MILLIS);
+ expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 4)
+ .addValues(TUN_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 1000L, 100L, 1000L, 100L, 2L)
+ .addValues(TUN_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 1000L, 100L, 1000L, 100L, 2L)
+ .addValues(TEST_IFACE, UID_VPN, SET_DEFAULT, TAG_NONE, 2200L, 200L, 2200L, 200L, 2L)
+ .addValues(
+ TEST_IFACE2, UID_VPN, SET_DEFAULT, TAG_NONE, 2200L, 200L, 2200L, 200L, 2L));
+
+ forcePollAndWaitForIdle();
+
+ assertUidTotal(sTemplateWifi, UID_RED, 500L, 50L, 500L, 50L, 1);
+ assertUidTotal(sTemplateWifi, UID_BLUE, 500L, 50L, 500L, 50L, 1);
+ assertUidTotal(sTemplateWifi, UID_VPN, 1200L, 100L, 1200L, 100L, 2);
+
+ assertUidTotal(buildTemplateMobileWildcard(), UID_RED, 500L, 50L, 500L, 50L, 1);
+ assertUidTotal(buildTemplateMobileWildcard(), UID_BLUE, 500L, 50L, 500L, 50L, 1);
+ assertUidTotal(buildTemplateMobileWildcard(), UID_VPN, 1200L, 100L, 1200L, 100L, 2);
+ }
+
+ @Test
+ public void vpnWithTwoUnderlyingIfaces_splitTraffic() throws Exception {
+ // WiFi and Cell networks are connected and VPN is using WiFi (which has TEST_IFACE) and
+ // Cell (which has TEST_IFACE2) and has declared both of them in its underlying network set.
+ // Additionally, VPN is arbitrarily splitting traffic across WiFi and Cell.
+ expectDefaultSettings();
+ NetworkState[] networkStates =
+ new NetworkState[] {
+ buildWifiState(), buildMobile4gState(TEST_IFACE2), buildVpnState()
+ };
+ VpnInfo[] vpnInfos = new VpnInfo[] {createVpnInfo(new String[] {TEST_IFACE, TEST_IFACE2})};
+ expectNetworkStatsUidDetail(buildEmptyStats());
+ expectBandwidthControlCheck();
+
+ mService.forceUpdateIfaces(
+ new Network[] {WIFI_NETWORK, VPN_NETWORK},
+ vpnInfos,
+ networkStates,
+ getActiveIface(networkStates));
+ // create some traffic (assume 10 bytes of MTU for VPN interface and 1 byte encryption
+ // overhead per packet):
+ // 1000 bytes (100 packets) were sent, and 500 bytes (50 packets) received by UID_RED over
+ // VPN.
+ // VPN sent 660 bytes (60 packets) over WiFi and 440 bytes (40 packets) over Cell.
+ // And, it received 330 bytes (30 packets) over WiFi and 220 bytes (20 packets) over Cell.
+ // For UID_RED, expect 600 bytes attributed over WiFi and 400 bytes over Cell for sent (tx)
+ // traffic. For received (rx) traffic, expect 300 bytes over WiFi and 200 bytes over Cell.
+ //
+ // For UID_VPN, expect 60 bytes attributed over WiFi and 40 bytes over Cell for tx traffic.
+ // And, 30 bytes over WiFi and 20 bytes over Cell for rx traffic.
+ incrementCurrentTime(HOUR_IN_MILLIS);
+ expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 3)
+ .addValues(TUN_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 500L, 50L, 1000L, 100L, 2L)
+ .addValues(TEST_IFACE, UID_VPN, SET_DEFAULT, TAG_NONE, 330L, 30L, 660L, 60L, 1L)
+ .addValues(TEST_IFACE2, UID_VPN, SET_DEFAULT, TAG_NONE, 220L, 20L, 440L, 40L, 1L));
+
+ forcePollAndWaitForIdle();
+
+ assertUidTotal(sTemplateWifi, UID_RED, 300L, 30L, 600L, 60L, 1);
+ assertUidTotal(sTemplateWifi, UID_VPN, 30L, 0L, 60L, 0L, 1);
+
+ assertUidTotal(buildTemplateMobileWildcard(), UID_RED, 200L, 20L, 400L, 40L, 1);
+ assertUidTotal(buildTemplateMobileWildcard(), UID_VPN, 20L, 0L, 40L, 0L, 1);
+ }
+
+ @Test
+ public void vpnWithTwoUnderlyingIfaces_splitTrafficWithCompression() throws Exception {
+ // WiFi and Cell networks are connected and VPN is using WiFi (which has TEST_IFACE) and
+ // Cell (which has TEST_IFACE2) and has declared both of them in its underlying network set.
+ // Additionally, VPN is arbitrarily splitting compressed traffic across WiFi and Cell.
+ expectDefaultSettings();
+ NetworkState[] networkStates =
+ new NetworkState[] {
+ buildWifiState(), buildMobile4gState(TEST_IFACE2), buildVpnState()
+ };
+ VpnInfo[] vpnInfos = new VpnInfo[] {createVpnInfo(new String[] {TEST_IFACE, TEST_IFACE2})};
+ expectNetworkStatsUidDetail(buildEmptyStats());
+ expectBandwidthControlCheck();
+
+ mService.forceUpdateIfaces(
+ new Network[] {WIFI_NETWORK, VPN_NETWORK},
+ vpnInfos,
+ networkStates,
+ getActiveIface(networkStates));
+ // create some traffic (assume 10 bytes of MTU for VPN interface:
+ // 1000 bytes (100 packets) were sent/received by UID_RED over VPN.
+ // VPN sent/received 600 bytes (60 packets) over WiFi and 200 bytes (20 packets) over Cell.
+ // For UID_RED, expect 600 bytes attributed over WiFi and 200 bytes over Cell for both
+ // rx/tx.
+ // UID_VPN gets nothing attributed to it (avoiding negative stats).
+ incrementCurrentTime(HOUR_IN_MILLIS);
+ expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 4)
+ .addValues(TUN_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 1000L, 100L, 1000L, 100L, 1L)
+ .addValues(TEST_IFACE, UID_VPN, SET_DEFAULT, TAG_NONE, 600L, 60L, 600L, 60L, 0L)
+ .addValues(TEST_IFACE2, UID_VPN, SET_DEFAULT, TAG_NONE, 200L, 20L, 200L, 20L, 0L));
+
+ forcePollAndWaitForIdle();
+
+ assertUidTotal(sTemplateWifi, UID_RED, 600L, 60L, 600L, 60L, 0);
+ assertUidTotal(sTemplateWifi, UID_VPN, 0L, 0L, 0L, 0L, 0);
+
+ assertUidTotal(buildTemplateMobileWildcard(), UID_RED, 200L, 20L, 200L, 20L, 0);
+ assertUidTotal(buildTemplateMobileWildcard(), UID_VPN, 0L, 0L, 0L, 0L, 0);
+ }
+
+ @Test
public void vpnWithIncorrectUnderlyingIface() throws Exception {
// WiFi and Cell networks are connected and VPN is using Cell (which has TEST_IFACE2),
// but has declared only WiFi (TEST_IFACE) in its underlying network set.
@@ -1001,7 +1248,7 @@
new NetworkState[] {
buildWifiState(), buildMobile4gState(TEST_IFACE2), buildVpnState()
};
- VpnInfo[] vpnInfos = new VpnInfo[] {createVpnInfo(TEST_IFACE)};
+ VpnInfo[] vpnInfos = new VpnInfo[] {createVpnInfo(new String[] {TEST_IFACE})};
expectNetworkStatsUidDetail(buildEmptyStats());
expectBandwidthControlCheck();
@@ -1030,6 +1277,134 @@
}
@Test
+ public void recordSnapshot_migratesTunTrafficAndUpdatesTunAdjustedStats() throws Exception {
+ assertEquals(0, mService.getTunAdjustedStats().size());
+ // VPN using WiFi (TEST_IFACE).
+ VpnInfo[] vpnInfos = new VpnInfo[] {createVpnInfo(new String[] {TEST_IFACE})};
+ expectBandwidthControlCheck();
+ // create some traffic (assume 10 bytes of MTU for VPN interface and 1 byte encryption
+ // overhead per packet):
+ // 1000 bytes (100 packets) were downloaded by UID_RED over VPN.
+ // VPN received 1100 bytes (100 packets) over WiFi.
+ incrementCurrentTime(HOUR_IN_MILLIS);
+ expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 2)
+ .addValues(TUN_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 1000L, 100L, 0L, 0L, 0L)
+ .addValues(TEST_IFACE, UID_VPN, SET_DEFAULT, TAG_NONE, 1100L, 100L, 0L, 0L, 0L));
+
+ // this should lead to NSS#recordSnapshotLocked
+ mService.forceUpdateIfaces(
+ new Network[0], vpnInfos, new NetworkState[0], null /* activeIface */);
+
+ // Verify TUN adjusted stats have traffic migrated correctly.
+ // Of 1100 bytes VPN received over WiFi, expect 1000 bytes attributed to UID_RED and 100
+ // bytes attributed to UID_VPN.
+ NetworkStats tunAdjStats = mService.getTunAdjustedStats();
+ assertValues(
+ tunAdjStats, TEST_IFACE, UID_RED, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL,
+ DEFAULT_NETWORK_ALL, 1000L, 100L, 0L, 0L, 0);
+ assertValues(
+ tunAdjStats, TEST_IFACE, UID_VPN, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL,
+ DEFAULT_NETWORK_ALL, 100L, 0L, 0L, 0L, 0);
+ }
+
+ @Test
+ public void getDetailedUidStats_migratesTunTrafficAndUpdatesTunAdjustedStats()
+ throws Exception {
+ assertEquals(0, mService.getTunAdjustedStats().size());
+ // VPN using WiFi (TEST_IFACE).
+ VpnInfo[] vpnInfos = new VpnInfo[] {createVpnInfo(new String[] {TEST_IFACE})};
+ expectBandwidthControlCheck();
+ mService.forceUpdateIfaces(
+ new Network[0], vpnInfos, new NetworkState[0], null /* activeIface */);
+ // create some traffic (assume 10 bytes of MTU for VPN interface and 1 byte encryption
+ // overhead per packet):
+ // 1000 bytes (100 packets) were downloaded by UID_RED over VPN.
+ // VPN received 1100 bytes (100 packets) over WiFi.
+ incrementCurrentTime(HOUR_IN_MILLIS);
+ expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 2)
+ .addValues(TUN_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 1000L, 100L, 0L, 0L, 0L)
+ .addValues(TEST_IFACE, UID_VPN, SET_DEFAULT, TAG_NONE, 1100L, 100L, 0L, 0L, 0L));
+
+ mService.getDetailedUidStats(INTERFACES_ALL);
+
+ // Verify internally maintained TUN adjusted stats
+ NetworkStats tunAdjStats = mService.getTunAdjustedStats();
+ // Verify stats for TEST_IFACE (WiFi):
+ // Of 1100 bytes VPN received over WiFi, expect 1000 bytes attributed to UID_RED and 100
+ // bytes attributed to UID_VPN.
+ assertValues(
+ tunAdjStats, TEST_IFACE, UID_RED, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL,
+ DEFAULT_NETWORK_ALL, 1000L, 100L, 0L, 0L, 0);
+ assertValues(
+ tunAdjStats, TEST_IFACE, UID_VPN, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL,
+ DEFAULT_NETWORK_ALL, 100L, 0L, 0L, 0L, 0);
+ // Verify stats for TUN_IFACE; only UID_RED should have usage on it.
+ assertValues(
+ tunAdjStats, TUN_IFACE, UID_RED, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL,
+ DEFAULT_NETWORK_ALL, 1000L, 100L, 0L, 0L, 0);
+ assertValues(
+ tunAdjStats, TUN_IFACE, UID_VPN, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL,
+ DEFAULT_NETWORK_ALL, 0L, 0L, 0L, 0L, 0);
+
+ // lets assume that since last time, VPN received another 1100 bytes (same assumptions as
+ // before i.e. UID_RED downloaded another 1000 bytes).
+ incrementCurrentTime(HOUR_IN_MILLIS);
+ // Note - NetworkStatsFactory returns counters that are monotonically increasing.
+ expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 2)
+ .addValues(TUN_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 2000L, 200L, 0L, 0L, 0L)
+ .addValues(TEST_IFACE, UID_VPN, SET_DEFAULT, TAG_NONE, 2200L, 200L, 0L, 0L, 0L));
+
+ mService.getDetailedUidStats(INTERFACES_ALL);
+
+ tunAdjStats = mService.getTunAdjustedStats();
+ // verify TEST_IFACE stats:
+ assertValues(
+ tunAdjStats, TEST_IFACE, UID_RED, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL,
+ DEFAULT_NETWORK_ALL, 2000L, 200L, 0L, 0L, 0);
+ assertValues(
+ tunAdjStats, TEST_IFACE, UID_VPN, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL,
+ DEFAULT_NETWORK_ALL, 200L, 0L, 0L, 0L, 0);
+ // verify TUN_IFACE stats:
+ assertValues(
+ tunAdjStats, TUN_IFACE, UID_RED, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL,
+ DEFAULT_NETWORK_ALL, 2000L, 200L, 0L, 0L, 0);
+ assertValues(
+ tunAdjStats, TUN_IFACE, UID_VPN, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL,
+ DEFAULT_NETWORK_ALL, 0L, 0L, 0L, 0L, 0);
+ }
+
+ @Test
+ public void getDetailedUidStats_returnsCorrectStatsWithVpnRunning() throws Exception {
+ // VPN using WiFi (TEST_IFACE).
+ VpnInfo[] vpnInfos = new VpnInfo[] {createVpnInfo(new String[] {TEST_IFACE})};
+ expectBandwidthControlCheck();
+ mService.forceUpdateIfaces(
+ new Network[0], vpnInfos, new NetworkState[0], null /* activeIface */);
+ // create some traffic (assume 10 bytes of MTU for VPN interface and 1 byte encryption
+ // overhead per packet):
+ // 1000 bytes (100 packets) were downloaded by UID_RED over VPN.
+ // VPN received 1100 bytes (100 packets) over WiFi.
+ incrementCurrentTime(HOUR_IN_MILLIS);
+ expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 2)
+ .addValues(TUN_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 1000L, 100L, 0L, 0L, 0L)
+ .addValues(TEST_IFACE, UID_VPN, SET_DEFAULT, TAG_NONE, 1100L, 100L, 0L, 0L, 0L));
+
+ // Query realtime stats for TEST_IFACE.
+ NetworkStats queriedStats =
+ mService.getDetailedUidStats(new String[] {TEST_IFACE});
+
+ assertEquals(HOUR_IN_MILLIS, queriedStats.getElapsedRealtime());
+ // verify that returned stats are only for TEST_IFACE and VPN traffic is migrated correctly.
+ assertEquals(new String[] {TEST_IFACE}, queriedStats.getUniqueIfaces());
+ assertValues(
+ queriedStats, TEST_IFACE, UID_RED, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL,
+ DEFAULT_NETWORK_ALL, 1000L, 100L, 0L, 0L, 0);
+ assertValues(
+ queriedStats, TEST_IFACE, UID_VPN, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL,
+ DEFAULT_NETWORK_ALL, 100L, 0L, 0L, 0L, 0);
+ }
+
+ @Test
public void testRegisterUsageCallback() throws Exception {
// pretend that wifi network comes online; service should ask about full
// network state, and poll any existing interfaces before updating.
@@ -1382,11 +1757,11 @@
return new NetworkState(info, prop, new NetworkCapabilities(), VPN_NETWORK, null, null);
}
- private static VpnInfo createVpnInfo(String underlyingIface) {
+ private static VpnInfo createVpnInfo(String[] underlyingIfaces) {
VpnInfo info = new VpnInfo();
info.ownerUid = UID_VPN;
info.vpnIface = TUN_IFACE;
- info.primaryUnderlyingIface = underlyingIface;
+ info.underlyingIfaces = underlyingIfaces;
return info;
}