Merge "create factory to generated an onClickListener for notifications such that CarSysUI can close the notification window when a notifcation launches an app"
diff --git a/api/current.txt b/api/current.txt
index 9d865b6..f773094 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -29677,8 +29677,8 @@
public class DiscoverySession implements java.lang.AutoCloseable {
method public void close();
- method public android.net.NetworkSpecifier createNetworkSpecifierOpen(android.net.wifi.aware.PeerHandle);
- method public android.net.NetworkSpecifier createNetworkSpecifierPassphrase(android.net.wifi.aware.PeerHandle, java.lang.String);
+ method public deprecated android.net.NetworkSpecifier createNetworkSpecifierOpen(android.net.wifi.aware.PeerHandle);
+ method public deprecated android.net.NetworkSpecifier createNetworkSpecifierPassphrase(android.net.wifi.aware.PeerHandle, java.lang.String);
method public void sendMessage(android.net.wifi.aware.PeerHandle, int, byte[]);
}
@@ -29763,6 +29763,21 @@
field public static final int WIFI_AWARE_DATA_PATH_ROLE_RESPONDER = 1; // 0x1
}
+ public static class WifiAwareManager.NetworkSpecifierBuilder {
+ ctor public WifiAwareManager.NetworkSpecifierBuilder();
+ method public android.net.NetworkSpecifier build();
+ method public android.net.wifi.aware.WifiAwareManager.NetworkSpecifierBuilder setDiscoverySession(android.net.wifi.aware.DiscoverySession);
+ method public android.net.wifi.aware.WifiAwareManager.NetworkSpecifierBuilder setPeerHandle(android.net.wifi.aware.PeerHandle);
+ method public android.net.wifi.aware.WifiAwareManager.NetworkSpecifierBuilder setPskPassphrase(java.lang.String);
+ }
+
+ public final class WifiAwareNetworkInfo implements android.os.Parcelable android.net.TransportInfo {
+ method public int describeContents();
+ method public java.net.Inet6Address getPeerIpv6Addr();
+ method public void writeToParcel(android.os.Parcel, int);
+ field public static final android.os.Parcelable.Creator<android.net.wifi.aware.WifiAwareNetworkInfo> CREATOR;
+ }
+
public class WifiAwareSession implements java.lang.AutoCloseable {
method public void close();
method public android.net.NetworkSpecifier createNetworkSpecifierOpen(int, byte[]);
@@ -42225,7 +42240,9 @@
field public static final int SIOCGIFBRDADDR;
field public static final int SIOCGIFDSTADDR;
field public static final int SIOCGIFNETMASK;
+ field public static final int SOCK_CLOEXEC;
field public static final int SOCK_DGRAM;
+ field public static final int SOCK_NONBLOCK;
field public static final int SOCK_RAW;
field public static final int SOCK_SEQPACKET;
field public static final int SOCK_STREAM;
@@ -44293,6 +44310,8 @@
method public void removeOnOpportunisticSubscriptionsChangedListener(android.telephony.SubscriptionManager.OnOpportunisticSubscriptionsChangedListener);
method public void removeOnSubscriptionsChangedListener(android.telephony.SubscriptionManager.OnSubscriptionsChangedListener);
method public boolean removeSubscriptionsFromGroup(int[]);
+ method public boolean setMetered(boolean, int);
+ method public boolean setOpportunistic(boolean, int);
method public java.lang.String setSubscriptionGroup(int[]);
method public void setSubscriptionOverrideCongested(int, boolean, long);
method public void setSubscriptionOverrideUnmetered(int, boolean, long);
@@ -56356,6 +56375,7 @@
method public boolean isCursorVisible();
method public boolean isElegantTextHeight();
method public boolean isFallbackLineSpacing();
+ method public final boolean isHorizontallyScrolling();
method public boolean isInputMethodTarget();
method public boolean isSingleLine();
method public boolean isSuggestionsEnabled();
diff --git a/api/system-current.txt b/api/system-current.txt
index cc93bba..017df40 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -2762,7 +2762,9 @@
method public deprecated boolean addGpsNavigationMessageListener(android.location.GpsNavigationMessageEvent.Listener);
method public void flushGnssBatch();
method public int getGnssBatchSize();
+ method public java.lang.String getLocationControllerExtraPackage();
method public java.lang.String getNetworkProviderPackage();
+ method public boolean isLocationControllerExtraPackageEnabled();
method public boolean isLocationEnabledForUser(android.os.UserHandle);
method public boolean isProviderEnabledForUser(java.lang.String, android.os.UserHandle);
method public boolean registerGnssBatchedLocationCallback(long, boolean, android.location.BatchedLocationCallback, android.os.Handler);
@@ -2770,6 +2772,8 @@
method public deprecated void removeGpsNavigationMessageListener(android.location.GpsNavigationMessageEvent.Listener);
method public void requestLocationUpdates(android.location.LocationRequest, android.location.LocationListener, android.os.Looper);
method public void requestLocationUpdates(android.location.LocationRequest, android.app.PendingIntent);
+ method public void setLocationControllerExtraPackage(java.lang.String);
+ method public void setLocationControllerExtraPackageEnabled(boolean);
method public void setLocationEnabledForUser(boolean, android.os.UserHandle);
method public boolean setProviderEnabledForUser(java.lang.String, boolean, android.os.UserHandle);
method public boolean unregisterGnssBatchedLocationCallback(android.location.BatchedLocationCallback);
@@ -3884,7 +3888,11 @@
package android.net.wifi.aware {
public class DiscoverySession implements java.lang.AutoCloseable {
- method public android.net.NetworkSpecifier createNetworkSpecifierPmk(android.net.wifi.aware.PeerHandle, byte[]);
+ method public deprecated android.net.NetworkSpecifier createNetworkSpecifierPmk(android.net.wifi.aware.PeerHandle, byte[]);
+ }
+
+ public static class WifiAwareManager.NetworkSpecifierBuilder {
+ method public android.net.wifi.aware.WifiAwareManager.NetworkSpecifierBuilder setPmk(byte[]);
}
public class WifiAwareSession implements java.lang.AutoCloseable {
@@ -4647,6 +4655,7 @@
public final class Settings {
field public static final java.lang.String ACTION_ENTERPRISE_PRIVACY_SETTINGS = "android.settings.ENTERPRISE_PRIVACY_SETTINGS";
+ field public static final java.lang.String ACTION_LOCATION_CONTROLLER_EXTRA_PACKAGE_SETTINGS = "android.settings.LOCATION_CONTROLLER_EXTRA_PACKAGE_SETTINGS";
field public static final java.lang.String ACTION_SHOW_ADMIN_SUPPORT_DETAILS = "android.settings.SHOW_ADMIN_SUPPORT_DETAILS";
}
@@ -5730,16 +5739,15 @@
public abstract class NetworkService extends android.app.Service {
ctor public NetworkService();
method protected abstract android.telephony.NetworkService.NetworkServiceProvider createNetworkServiceProvider(int);
- field public static final java.lang.String NETWORK_SERVICE_EXTRA_SLOT_ID = "android.telephony.extra.SLOT_ID";
field public static final java.lang.String NETWORK_SERVICE_INTERFACE = "android.telephony.NetworkService";
}
- public class NetworkService.NetworkServiceProvider {
+ public abstract class NetworkService.NetworkServiceProvider implements java.lang.AutoCloseable {
ctor public NetworkService.NetworkServiceProvider(int);
+ method public abstract void close();
method public void getNetworkRegistrationState(int, android.telephony.NetworkServiceCallback);
method public final int getSlotId();
method public final void notifyNetworkRegistrationStateChanged();
- method protected void onDestroy();
}
public class NetworkServiceCallback {
@@ -6079,20 +6087,19 @@
public abstract class DataService extends android.app.Service {
ctor public DataService();
method public abstract android.telephony.data.DataService.DataServiceProvider createDataServiceProvider(int);
- field public static final java.lang.String DATA_SERVICE_EXTRA_SLOT_ID = "android.telephony.data.extra.SLOT_ID";
field public static final java.lang.String DATA_SERVICE_INTERFACE = "android.telephony.data.DataService";
field public static final int REQUEST_REASON_HANDOVER = 3; // 0x3
field public static final int REQUEST_REASON_NORMAL = 1; // 0x1
field public static final int REQUEST_REASON_SHUTDOWN = 2; // 0x2
}
- public class DataService.DataServiceProvider {
+ public abstract class DataService.DataServiceProvider implements java.lang.AutoCloseable {
ctor public DataService.DataServiceProvider(int);
+ method public abstract void close();
method public void deactivateDataCall(int, int, android.telephony.data.DataServiceCallback);
method public void getDataCallList(android.telephony.data.DataServiceCallback);
method public final int getSlotId();
method public final void notifyDataCallListChanged(java.util.List<android.telephony.data.DataCallResponse>);
- method protected void onDestroy();
method public void setDataProfile(java.util.List<android.telephony.data.DataProfile>, boolean, android.telephony.data.DataServiceCallback);
method public void setInitialAttachApn(android.telephony.data.DataProfile, boolean, android.telephony.data.DataServiceCallback);
method public void setupDataCall(int, android.telephony.data.DataProfile, boolean, boolean, int, android.net.LinkProperties, android.telephony.data.DataServiceCallback);
@@ -6614,8 +6621,8 @@
field public static final int CODE_SIP_SERVER_TIMEOUT = 353; // 0x161
field public static final int CODE_SIP_SERVICE_UNAVAILABLE = 352; // 0x160
field public static final int CODE_SIP_TEMPRARILY_UNAVAILABLE = 336; // 0x150
- field public static final int CODE_SIP_TRANSACTION_DOES_NOT_EXIST = 343; // 0x157
field public static final int CODE_SIP_TOO_MANY_HOPS = 374; // 0x176
+ field public static final int CODE_SIP_TRANSACTION_DOES_NOT_EXIST = 343; // 0x157
field public static final int CODE_SIP_UNDECIPHERABLE = 378; // 0x17a
field public static final int CODE_SIP_USER_MARKED_UNWANTED = 365; // 0x16d
field public static final int CODE_SIP_USER_REJECTED = 361; // 0x169
diff --git a/cmds/statsd/src/atoms.proto b/cmds/statsd/src/atoms.proto
index 410bd19..9ee0786 100644
--- a/cmds/statsd/src/atoms.proto
+++ b/cmds/statsd/src/atoms.proto
@@ -1222,7 +1222,7 @@
STATE_CONNECTED = 1;
}
optional State state = 6;
- optional int64 last_connect_duration_ms = 7;
+ optional int64 last_connect_duration_millis = 7;
}
diff --git a/core/java/android/app/job/JobInfo.java b/core/java/android/app/job/JobInfo.java
index e84517d..e7fe161 100644
--- a/core/java/android/app/job/JobInfo.java
+++ b/core/java/android/app/job/JobInfo.java
@@ -1545,13 +1545,6 @@
* @return The job object to hand to the JobScheduler. This object is immutable.
*/
public JobInfo build() {
- // Allow jobs with no constraints - What am I, a database?
- if (!mHasEarlyConstraint && !mHasLateConstraint && mConstraintFlags == 0 &&
- mNetworkRequest == null &&
- mTriggerContentUris == null) {
- throw new IllegalArgumentException("You're trying to build a job with no " +
- "constraints, this is not allowed.");
- }
// Check that network estimates require network type
if ((mNetworkDownloadBytes > 0 || mNetworkUploadBytes > 0) && mNetworkRequest == null) {
throw new IllegalArgumentException(
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index cbcc492..3e451fe 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -150,6 +150,23 @@
"android.settings.LOCATION_SOURCE_SETTINGS";
/**
+ * Activity Action: Show settings to allow configuration of location controller extra package.
+ * <p>
+ * In some cases, a matching Activity may not exist, so ensure you
+ * safeguard against this.
+ * <p>
+ * Input: Nothing.
+ * <p>
+ * Output: Nothing.
+ *
+ * @hide
+ */
+ @SystemApi
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_LOCATION_CONTROLLER_EXTRA_PACKAGE_SETTINGS =
+ "android.settings.LOCATION_CONTROLLER_EXTRA_PACKAGE_SETTINGS";
+
+ /**
* Activity Action: Show scanning settings to allow configuration of Wi-Fi
* and Bluetooth scanning settings.
* <p>
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 9fe0ddc..9a1e931 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -1876,6 +1876,7 @@
}
void dispatchApplyInsets(View host) {
+ Trace.traceBegin(Trace.TRACE_TAG_VIEW, "dispatchApplyInsets");
WindowInsets insets = getWindowInsets(true /* forceConstruct */);
final boolean dispatchCutout = (mWindowAttributes.layoutInDisplayCutoutMode
== LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS);
@@ -1885,6 +1886,7 @@
insets = insets.consumeDisplayCutout();
}
host.dispatchApplyWindowInsets(insets);
+ Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
InsetsController getInsetsController() {
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 90da812..ddf1190 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -719,7 +719,7 @@
@ViewDebug.ExportedProperty(category = "text")
@UnsupportedAppUsage
private int mGravity = Gravity.TOP | Gravity.START;
- @UnsupportedAppUsage
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
private boolean mHorizontallyScrolling;
private int mAutoLinkMask;
@@ -5095,13 +5095,24 @@
}
/**
- * Returns whether the text is allowed to be wider than the View is.
+ * Returns whether the text is allowed to be wider than the View.
+ * If false, the text will be wrapped to the width of the View.
+ *
+ * @attr ref android.R.styleable#TextView_scrollHorizontally
+ * @see #setHorizontallyScrolling(boolean)
+ */
+ public final boolean isHorizontallyScrolling() {
+ return mHorizontallyScrolling;
+ }
+
+ /**
+ * Returns whether the text is allowed to be wider than the View.
* If false, the text will be wrapped to the width of the View.
*
* @attr ref android.R.styleable#TextView_scrollHorizontally
* @hide
*/
- @UnsupportedAppUsage
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
public boolean getHorizontallyScrolling() {
return mHorizontallyScrolling;
}
diff --git a/core/jni/android/graphics/Paint.cpp b/core/jni/android/graphics/Paint.cpp
index df24bc4..e97c9bc 100644
--- a/core/jni/android/graphics/Paint.cpp
+++ b/core/jni/android/graphics/Paint.cpp
@@ -577,7 +577,7 @@
}
}
- static SkScalar getMetricsInternal(jlong paintHandle, Paint::FontMetrics *metrics) {
+ static SkScalar getMetricsInternal(jlong paintHandle, SkFontMetrics *metrics) {
const int kElegantTop = 2500;
const int kElegantBottom = -1000;
const int kElegantAscent = 1900;
@@ -609,7 +609,7 @@
}
static jfloat getFontMetrics(JNIEnv* env, jobject, jlong paintHandle, jobject metricsObj) {
- Paint::FontMetrics metrics;
+ SkFontMetrics metrics;
SkScalar spacing = getMetricsInternal(paintHandle, &metrics);
if (metricsObj) {
@@ -624,7 +624,7 @@
}
static jint getFontMetricsInt(JNIEnv* env, jobject, jlong paintHandle, jobject metricsObj) {
- Paint::FontMetrics metrics;
+ SkFontMetrics metrics;
getMetricsInternal(paintHandle, &metrics);
int ascent = SkScalarRoundToInt(metrics.fAscent);
@@ -970,19 +970,19 @@
}
static jfloat ascent(jlong paintHandle) {
- Paint::FontMetrics metrics;
+ SkFontMetrics metrics;
getMetricsInternal(paintHandle, &metrics);
return SkScalarToFloat(metrics.fAscent);
}
static jfloat descent(jlong paintHandle) {
- Paint::FontMetrics metrics;
+ SkFontMetrics metrics;
getMetricsInternal(paintHandle, &metrics);
return SkScalarToFloat(metrics.fDescent);
}
static jfloat getUnderlinePosition(jlong paintHandle) {
- Paint::FontMetrics metrics;
+ SkFontMetrics metrics;
getMetricsInternal(paintHandle, &metrics);
SkScalar position;
if (metrics.hasUnderlinePosition(&position)) {
@@ -994,7 +994,7 @@
}
static jfloat getUnderlineThickness(jlong paintHandle) {
- Paint::FontMetrics metrics;
+ SkFontMetrics metrics;
getMetricsInternal(paintHandle, &metrics);
SkScalar thickness;
if (metrics.hasUnderlineThickness(&thickness)) {
diff --git a/core/proto/android/server/jobscheduler.proto b/core/proto/android/server/jobscheduler.proto
index 7fe3be8..0ec8c1a 100644
--- a/core/proto/android/server/jobscheduler.proto
+++ b/core/proto/android/server/jobscheduler.proto
@@ -220,6 +220,13 @@
// will use heartbeats, false will use a rolling window.
optional bool use_heartbeats = 23;
+ message TimeController {
+ // Whether or not TimeController should skip setting wakeup alarms for jobs that aren't
+ // ready now.
+ optional bool skip_not_ready_jobs = 1;
+ }
+ optional TimeController time_controller = 25;
+
message QuotaController {
// How much time each app will have to run jobs within their standby bucket window.
optional int64 allowed_time_per_period_ms = 1;
@@ -242,8 +249,12 @@
// expected to run only {@link QUOTA_CONTROLLER_ALLOWED_TIME_PER_PERIOD_MS} within the past
// WINDOW_SIZE_MS.
optional int64 rare_window_size_ms = 6;
+ // The maximum amount of time an app can have its jobs running within a 24 hour window.
+ optional int64 max_execution_time_ms = 7;
}
optional QuotaController quota_controller = 24;
+
+ // Next tag: 26
}
message StateControllerProto {
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index cab01f9..f25427a 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -3200,6 +3200,8 @@
<!-- [CHAR LIMIT=40] Title of dialog that is shown when system is starting. -->
<string name="android_start_title" product="default">Phone is starting\u2026</string>
<!-- [CHAR LIMIT=40] Title of dialog that is shown when system is starting. -->
+ <string name="android_start_title" product="automotive">Android is starting\u2026</string>
+ <!-- [CHAR LIMIT=40] Title of dialog that is shown when system is starting. -->
<string name="android_start_title" product="tablet">Tablet is starting\u2026</string>
<!-- [CHAR LIMIT=40] Title of dialog that is shown when system is starting. -->
<string name="android_start_title" product="device">Device is starting\u2026</string>
diff --git a/libs/hwui/hwui/Canvas.cpp b/libs/hwui/hwui/Canvas.cpp
index e2ea2bc..a09da6b 100644
--- a/libs/hwui/hwui/Canvas.cpp
+++ b/libs/hwui/hwui/Canvas.cpp
@@ -52,7 +52,7 @@
const SkScalar left = x;
const SkScalar right = x + length;
if (flags & SkPaint::kUnderlineText_ReserveFlag) {
- Paint::FontMetrics metrics;
+ SkFontMetrics metrics;
paint.getFontMetrics(&metrics);
SkScalar position;
if (!metrics.hasUnderlinePosition(&position)) {
diff --git a/location/java/android/location/ILocationManager.aidl b/location/java/android/location/ILocationManager.aidl
index 32c7520..05d49e5 100644
--- a/location/java/android/location/ILocationManager.aidl
+++ b/location/java/android/location/ILocationManager.aidl
@@ -88,6 +88,10 @@
boolean providerMeetsCriteria(String provider, in Criteria criteria);
ProviderProperties getProviderProperties(String provider);
String getNetworkProviderPackage();
+ void setLocationControllerExtraPackage(String packageName);
+ String getLocationControllerExtraPackage();
+ void setLocationControllerExtraPackageEnabled(boolean enabled);
+ boolean isLocationControllerExtraPackageEnabled();
boolean isProviderEnabledForUser(String provider, int userId);
boolean setProviderEnabledForUser(String provider, boolean enabled, int userId);
diff --git a/location/java/android/location/LocationManager.java b/location/java/android/location/LocationManager.java
index 334170e..1cd3d86 100644
--- a/location/java/android/location/LocationManager.java
+++ b/location/java/android/location/LocationManager.java
@@ -2396,4 +2396,65 @@
return null;
}
}
+
+ /**
+ * Set the extra location controller package for location services on the device.
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(Manifest.permission.LOCATION_HARDWARE)
+ public void setLocationControllerExtraPackage(String packageName) {
+ try {
+ mService.setLocationControllerExtraPackage(packageName);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns the extra location controller package on the device.
+ *
+ * @hide
+ */
+ @SystemApi
+ public @Nullable String getLocationControllerExtraPackage() {
+ try {
+ return mService.getLocationControllerExtraPackage();
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ return null;
+ }
+ }
+
+ /**
+ * Set whether the extra location controller package is currently enabled on the device.
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(Manifest.permission.LOCATION_HARDWARE)
+ public void setLocationControllerExtraPackageEnabled(boolean enabled) {
+ try {
+ mService.setLocationControllerExtraPackageEnabled(enabled);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns whether extra location controller package is currently enabled on the device.
+ *
+ * @hide
+ */
+ @SystemApi
+ public boolean isLocationControllerExtraPackageEnabled() {
+ try {
+ return mService.isLocationControllerExtraPackageEnabled();
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ return false;
+ }
+ }
+
}
diff --git a/media/java/android/media/DataSourceCallback.java b/media/java/android/media/DataSourceCallback.java
index 0d4d531..1afcd20 100644
--- a/media/java/android/media/DataSourceCallback.java
+++ b/media/java/android/media/DataSourceCallback.java
@@ -47,7 +47,7 @@
* @param offset the offset within buffer to read the data into.
* @param size the number of bytes to read.
* @throws IOException on fatal errors.
- * @return the number of bytes read, or -1 if there was an error.
+ * @return the number of bytes read, or -1 if end of stream is reached.
*/
public abstract int readAt(long position, byte[] buffer, int offset, int size)
throws IOException;
diff --git a/media/java/android/media/MediaDataSource.java b/media/java/android/media/MediaDataSource.java
index 4ba2120..4bdc1ad 100644
--- a/media/java/android/media/MediaDataSource.java
+++ b/media/java/android/media/MediaDataSource.java
@@ -46,7 +46,7 @@
* @param offset the offset within buffer to read the data into.
* @param size the number of bytes to read.
* @throws IOException on fatal errors.
- * @return the number of bytes read, or -1 if there was an error.
+ * @return the number of bytes read, or -1 if end of stream is reached.
*/
public abstract int readAt(long position, byte[] buffer, int offset, int size)
throws IOException;
diff --git a/packages/CarSystemUI/src/com/android/systemui/volume/CarVolumeDialogImpl.java b/packages/CarSystemUI/src/com/android/systemui/volume/CarVolumeDialogImpl.java
index 1737b64..85dab57 100644
--- a/packages/CarSystemUI/src/com/android/systemui/volume/CarVolumeDialogImpl.java
+++ b/packages/CarSystemUI/src/com/android/systemui/volume/CarVolumeDialogImpl.java
@@ -107,36 +107,44 @@
private CarAudioManager mCarAudioManager;
private final CarAudioManager.CarVolumeCallback mVolumeChangeCallback =
new CarAudioManager.CarVolumeCallback() {
- @Override
- public void onGroupVolumeChanged(int zoneId, int groupId, int flags) {
- // TODO: Include zoneId into consideration.
- // For instance
- // - single display + single-zone, ignore zoneId
- // - multi-display + single-zone, zoneId is fixed, may show volume bar on all displays
- // - single-display + multi-zone, may show volume bar on primary display only
- // - multi-display + multi-zone, may show volume bar on display specified by zoneId
- VolumeItem volumeItem = mAvailableVolumeItems.get(groupId);
- int value = getSeekbarValue(mCarAudioManager, groupId);
- // Do not update the progress if it is the same as before. When car audio manager sets
- // its group volume caused by the seekbar progress changed, it also triggers this
- // callback. Updating the seekbar at the same time could block the continuous seeking.
- if (value != volumeItem.progress) {
- volumeItem.listItem.setProgress(value);
- volumeItem.progress = value;
- }
- if ((flags & AudioManager.FLAG_SHOW_UI) != 0) {
- mHandler.obtainMessage(H.SHOW, Events.SHOW_REASON_VOLUME_CHANGED).sendToTarget();
- }
- }
+ @Override
+ public void onGroupVolumeChanged(int zoneId, int groupId, int flags) {
+ // TODO: Include zoneId into consideration.
+ // For instance
+ // - single display + single-zone, ignore zoneId
+ // - multi-display + single-zone, zoneId is fixed, may show volume bar on all
+ // displays
+ // - single-display + multi-zone, may show volume bar on primary display only
+ // - multi-display + multi-zone, may show volume bar on display specified by
+ // zoneId
+ VolumeItem volumeItem = mAvailableVolumeItems.get(groupId);
+ int value = getSeekbarValue(mCarAudioManager, groupId);
+ // Do not update the progress if it is the same as before. When car audio
+ // manager sets
+ // its group volume caused by the seekbar progress changed, it also triggers
+ // this
+ // callback. Updating the seekbar at the same time could block the continuous
+ // seeking.
+ if (value != volumeItem.progress) {
+ volumeItem.listItem.setProgress(value);
+ volumeItem.progress = value;
+ }
+ if ((flags & AudioManager.FLAG_SHOW_UI) != 0) {
+ mHandler.obtainMessage(H.SHOW,
+ Events.SHOW_REASON_VOLUME_CHANGED).sendToTarget();
+ }
+ }
- @Override
- public void onMasterMuteChanged(int zoneId, int flags) {
- // ignored
- }
- };
+ @Override
+ public void onMasterMuteChanged(int zoneId, int flags) {
+ // ignored
+ }
+ };
private boolean mHovering;
private boolean mShowing;
private boolean mExpanded;
+ private View mExpandIcon;
+ private VolumeItem mDefaultVolumeItem;
private final ServiceConnection mServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
@@ -151,10 +159,8 @@
mAvailableVolumeItems.add(volumeItem);
// The first one is the default item.
if (groupId == 0) {
- volumeItem.defaultItem = true;
- addSeekbarListItem(volumeItem, groupId,
- R.drawable.car_ic_keyboard_arrow_down,
- new ExpandIconListener());
+ mDefaultVolumeItem = volumeItem;
+ setupDefaultListItem();
}
}
@@ -178,6 +184,13 @@
}
};
+ private void setupDefaultListItem() {
+ mDefaultVolumeItem.defaultItem = true;
+ addSeekbarListItem(mDefaultVolumeItem, /* volumeGroupId = */0,
+ R.drawable.car_ic_keyboard_arrow_down, new ExpandIconListener()
+ );
+ }
+
public CarVolumeDialogImpl(Context context) {
mContext = new ContextThemeWrapper(context, com.android.systemui.R.style.qs_theme);
mKeyguard = (KeyguardManager) mContext.getSystemService(Context.KEYGUARD_SERVICE);
@@ -294,7 +307,9 @@
return;
}
mShowing = true;
-
+ if (mVolumeLineItems.isEmpty()) {
+ setupDefaultListItem();
+ }
mDialog.show();
Events.writeEvent(mContext, Events.EVENT_SHOW_DIALOG, reason, mKeyguard.isKeyguardLocked());
}
@@ -340,6 +355,13 @@
}
mDialog.dismiss();
mShowing = false;
+ mShowing = false;
+ // if mExpandIcon is null that means user never clicked on the expanded arrow
+ // which implies that the dialog is still not expanded. In that case we do
+ // not want to reset the state
+ if (mExpandIcon != null && mExpanded) {
+ toggleDialogExpansion(/* isClicked = */ false);
+ }
}, DISMISS_DELAY_IN_MILLIS))
.start();
@@ -517,52 +539,62 @@
}
private final class ExpandIconListener implements View.OnClickListener {
-
@Override
public void onClick(final View v) {
- mExpanded = !mExpanded;
- Animator inAnimator;
- if (mExpanded) {
- for (int groupId = 0; groupId < mAvailableVolumeItems.size(); ++groupId) {
- // Adding the items which are not coming from the default item.
- VolumeItem volumeItem = mAvailableVolumeItems.get(groupId);
- if (volumeItem.defaultItem) {
- // Set progress here due to the progress of seekbar may not be updated.
- volumeItem.listItem.setProgress(volumeItem.progress);
- } else {
- addSeekbarListItem(volumeItem, groupId, 0, null);
- }
- }
- inAnimator = AnimatorInflater.loadAnimator(
- mContext, R.anim.car_arrow_fade_in_rotate_up);
- } else {
- // Only keeping the default stream if it is not expended.
- Iterator itr = mVolumeLineItems.iterator();
- while (itr.hasNext()) {
- SeekbarListItem seekbarListItem = (SeekbarListItem) itr.next();
- VolumeItem volumeItem = findVolumeItem(seekbarListItem);
- if (!volumeItem.defaultItem) {
- itr.remove();
- } else {
- // Set progress here due to the progress of seekbar may not be updated.
- seekbarListItem.setProgress(volumeItem.progress);
- }
- }
- inAnimator = AnimatorInflater.loadAnimator(
- mContext, R.anim.car_arrow_fade_in_rotate_down);
- }
-
- Animator outAnimator = AnimatorInflater.loadAnimator(
- mContext, R.anim.car_arrow_fade_out);
- inAnimator.setStartDelay(ARROW_FADE_IN_START_DELAY_IN_MILLIS);
- AnimatorSet animators = new AnimatorSet();
- animators.playTogether(outAnimator, inAnimator);
- animators.setTarget(v);
- animators.start();
- mPagedListAdapter.notifyDataSetChanged();
+ mExpandIcon = v;
+ toggleDialogExpansion(true);
}
}
+ private void toggleDialogExpansion(boolean isClicked) {
+ mExpanded = !mExpanded;
+ Animator inAnimator;
+ if (mExpanded) {
+ for (int groupId = 0; groupId < mAvailableVolumeItems.size(); ++groupId) {
+ // Adding the items which are not coming from the default item.
+ VolumeItem volumeItem = mAvailableVolumeItems.get(groupId);
+ if (volumeItem.defaultItem) {
+ // Set progress here due to the progress of seekbar may not be updated.
+ volumeItem.listItem.setProgress(volumeItem.progress);
+ } else {
+ addSeekbarListItem(volumeItem, groupId, 0, null);
+ }
+ }
+ inAnimator = AnimatorInflater.loadAnimator(
+ mContext, R.anim.car_arrow_fade_in_rotate_up);
+
+ } else {
+ // Only keeping the default stream if it is not expended.
+ Iterator itr = mVolumeLineItems.iterator();
+ while (itr.hasNext()) {
+ SeekbarListItem seekbarListItem = (SeekbarListItem) itr.next();
+ VolumeItem volumeItem = findVolumeItem(seekbarListItem);
+ if (!volumeItem.defaultItem) {
+ itr.remove();
+ } else {
+ // Set progress here due to the progress of seekbar may not be updated.
+ seekbarListItem.setProgress(volumeItem.progress);
+ }
+ }
+ inAnimator = AnimatorInflater.loadAnimator(
+ mContext, R.anim.car_arrow_fade_in_rotate_down);
+ }
+
+ Animator outAnimator = AnimatorInflater.loadAnimator(
+ mContext, R.anim.car_arrow_fade_out);
+ inAnimator.setStartDelay(ARROW_FADE_IN_START_DELAY_IN_MILLIS);
+ AnimatorSet animators = new AnimatorSet();
+ animators.playTogether(outAnimator, inAnimator);
+ if (!isClicked) {
+ // Do not animate when the state is called to reset the dialogs view and not clicked
+ // by user.
+ animators.setDuration(0);
+ }
+ animators.setTarget(mExpandIcon);
+ animators.start();
+ mPagedListAdapter.notifyDataSetChanged();
+ }
+
private final class VolumeSeekBarChangeListener implements OnSeekBarChangeListener {
private final int mVolumeGroupId;
@@ -601,4 +633,4 @@
public void onStopTrackingTouch(SeekBar seekBar) {
}
}
-}
+}
\ No newline at end of file
diff --git a/packages/SettingsLib/ActionButtonsPreference/Android.bp b/packages/SettingsLib/ActionButtonsPreference/Android.bp
index e518e0b..cd3fb0c 100644
--- a/packages/SettingsLib/ActionButtonsPreference/Android.bp
+++ b/packages/SettingsLib/ActionButtonsPreference/Android.bp
@@ -1,5 +1,5 @@
android_library {
- name: "ActionButtonsPreference",
+ name: "SettingsLibActionButtonsPreference",
srcs: ["src/**/*.java"],
resource_dirs: ["res"],
diff --git a/packages/SettingsLib/Android.bp b/packages/SettingsLib/Android.bp
index 0126e7e5..042808a0 100644
--- a/packages/SettingsLib/Android.bp
+++ b/packages/SettingsLib/Android.bp
@@ -16,8 +16,8 @@
"SettingsLibAppPreference",
"SettingsLibSearchWidget",
"SettingsLibSettingsSpinner",
- "SettingsLayoutPreference",
- "ActionButtonsPreference",
+ "SettingsLibLayoutPreference",
+ "SettingsLibActionButtonsPreference",
"SettingsLibEntityHeaderWidgets",
],
diff --git a/packages/SettingsLib/SettingsLayoutPreference/Android.bp b/packages/SettingsLib/LayoutPreference/Android.bp
similarity index 83%
rename from packages/SettingsLib/SettingsLayoutPreference/Android.bp
rename to packages/SettingsLib/LayoutPreference/Android.bp
index 489d360..a1f9a76 100644
--- a/packages/SettingsLib/SettingsLayoutPreference/Android.bp
+++ b/packages/SettingsLib/LayoutPreference/Android.bp
@@ -1,5 +1,5 @@
android_library {
- name: "SettingsLayoutPreference",
+ name: "SettingsLibLayoutPreference",
srcs: ["src/**/*.java"],
resource_dirs: ["res"],
diff --git a/packages/SettingsLib/SettingsLayoutPreference/AndroidManifest.xml b/packages/SettingsLib/LayoutPreference/AndroidManifest.xml
similarity index 100%
rename from packages/SettingsLib/SettingsLayoutPreference/AndroidManifest.xml
rename to packages/SettingsLib/LayoutPreference/AndroidManifest.xml
diff --git a/packages/SettingsLib/SettingsLayoutPreference/res/layout/layout_preference_frame.xml b/packages/SettingsLib/LayoutPreference/res/layout/layout_preference_frame.xml
similarity index 100%
rename from packages/SettingsLib/SettingsLayoutPreference/res/layout/layout_preference_frame.xml
rename to packages/SettingsLib/LayoutPreference/res/layout/layout_preference_frame.xml
diff --git a/packages/SettingsLib/SettingsLayoutPreference/res/layout/settings_entity_header.xml b/packages/SettingsLib/LayoutPreference/res/layout/settings_entity_header.xml
similarity index 100%
rename from packages/SettingsLib/SettingsLayoutPreference/res/layout/settings_entity_header.xml
rename to packages/SettingsLib/LayoutPreference/res/layout/settings_entity_header.xml
diff --git a/packages/SettingsLib/SettingsLayoutPreference/res/values/styles.xml b/packages/SettingsLib/LayoutPreference/res/values/styles.xml
similarity index 100%
rename from packages/SettingsLib/SettingsLayoutPreference/res/values/styles.xml
rename to packages/SettingsLib/LayoutPreference/res/values/styles.xml
diff --git a/packages/SettingsLib/SettingsLayoutPreference/src/com/android/settingslib/widget/LayoutPreference.java b/packages/SettingsLib/LayoutPreference/src/com/android/settingslib/widget/LayoutPreference.java
similarity index 100%
rename from packages/SettingsLib/SettingsLayoutPreference/src/com/android/settingslib/widget/LayoutPreference.java
rename to packages/SettingsLib/LayoutPreference/src/com/android/settingslib/widget/LayoutPreference.java
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/AlertTransferListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/AlertTransferListener.java
new file mode 100644
index 0000000..13e991b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/AlertTransferListener.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2018 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;
+
+/**
+ * Listener interface for when NotificationEntryManager needs to tell
+ * NotificationGroupAlertTransferHelper things. Will eventually grow to be a general-purpose
+ * listening interface for the NotificationEntryManager.
+ */
+public interface AlertTransferListener {
+ /**
+ * Called when a new notification is posted. At this point, the notification is "pending": its
+ * views haven't been inflated yet and most of the system pretends like it doesn't exist yet.
+ */
+ void onPendingEntryAdded(NotificationData.Entry entry);
+
+ /**
+ * Called when an existing notification's views are reinflated (usually due to an update being
+ * posted to that notification).
+ */
+ void onEntryReinflated(NotificationData.Entry entry);
+
+ /**
+ * Called when a notification has been removed (either because the user swiped it away or
+ * because the developer retracted it).
+ */
+ void onEntryRemoved(NotificationData.Entry entry);
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java
index 5e244f8..aab3fc4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java
@@ -62,7 +62,6 @@
import com.android.systemui.Dumpable;
import com.android.systemui.EventLogTags;
import com.android.systemui.ForegroundServiceController;
-import com.android.systemui.InitController;
import com.android.systemui.R;
import com.android.systemui.UiOffloadThread;
import com.android.systemui.bubbles.BubbleController;
@@ -84,7 +83,6 @@
import com.android.systemui.statusbar.notification.row.NotificationInflater.InflationFlag;
import com.android.systemui.statusbar.notification.row.RowInflaterTask;
import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
-import com.android.systemui.statusbar.phone.NotificationGroupAlertTransferHelper;
import com.android.systemui.statusbar.phone.NotificationGroupManager;
import com.android.systemui.statusbar.phone.ShadeController;
import com.android.systemui.statusbar.phone.StatusBar;
@@ -121,8 +119,6 @@
private final NotificationGroupManager mGroupManager =
Dependency.get(NotificationGroupManager.class);
- private final NotificationGroupAlertTransferHelper mGroupAlertTransferHelper =
- Dependency.get(NotificationGroupAlertTransferHelper.class);
private final NotificationGutsManager mGutsManager =
Dependency.get(NotificationGutsManager.class);
private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class);
@@ -164,6 +160,7 @@
= new ArrayList<>();
private ExpandableNotificationRow.OnAppOpsClickListener mOnAppOpsClickListener;
private NotificationViewHierarchyManager.StatusBarStateListener mStatusBarStateListener;
+ @Nullable private AlertTransferListener mAlertTransferListener;
private final class NotificationClicker implements View.OnClickListener {
@@ -265,13 +262,11 @@
mMessagingUtil = new NotificationMessagingUtil(context);
mBubbleController.setDismissListener(this /* bubbleEventListener */);
mNotificationData = new NotificationData();
- Dependency.get(InitController.class).addPostInitTask(this::onPostInit);
mDeferredNotificationViewUpdateHandler = new Handler();
}
- private void onPostInit() {
- mGroupAlertTransferHelper.setPendingEntries(mPendingNotifications);
- mGroupManager.addOnGroupChangeListener(mGroupAlertTransferHelper);
+ public void setAlertTransferListener(AlertTransferListener listener) {
+ mAlertTransferListener = listener;
}
/**
@@ -607,7 +602,9 @@
mVisualStabilityManager.onLowPriorityUpdated(entry);
mPresenter.updateNotificationViews();
}
- mGroupAlertTransferHelper.onInflationFinished(entry);
+ if (mAlertTransferListener != null) {
+ mAlertTransferListener.onEntryReinflated(entry);
+ }
}
}
entry.setLowPriorityStateUpdated(false);
@@ -620,8 +617,12 @@
private void removeNotificationInternal(String key,
@Nullable NotificationListenerService.RankingMap ranking, boolean forceRemove) {
+ final NotificationData.Entry entry = mNotificationData.get(key);
+
abortExistingInflation(key);
- mGroupAlertTransferHelper.cleanUpPendingAlertInfo(key);
+ if (mAlertTransferListener != null && entry != null) {
+ mAlertTransferListener.onEntryRemoved(entry);
+ }
// Attempt to remove notifications from their alert managers (heads up, ambient pulse).
// Though the remove itself may fail, it lets the manager know to remove as soon as
@@ -640,8 +641,6 @@
mAmbientPulseManager.removeNotification(key, false /* ignoreEarliestRemovalTime */);
}
- NotificationData.Entry entry = mNotificationData.get(key);
-
if (entry == null) {
mCallback.onNotificationRemoved(key, null /* old */);
return;
@@ -866,7 +865,9 @@
mNotificationData.getImportance(key));
mPendingNotifications.put(key, shadeEntry);
- mGroupAlertTransferHelper.onPendingEntryAdded(shadeEntry);
+ if (mAlertTransferListener != null) {
+ mAlertTransferListener.onPendingEntryAdded(shadeEntry);
+ }
}
@VisibleForTesting
@@ -1253,6 +1254,15 @@
}
/**
+ * @return An iterator for all "pending" notifications. Pending notifications are newly-posted
+ * notifications whose views have not yet been inflated. In general, the system pretends like
+ * these don't exist, although there are a couple exceptions.
+ */
+ public Iterable<NotificationData.Entry> getPendingNotificationsIterator() {
+ return mPendingNotifications.values();
+ }
+
+ /**
* Callback for NotificationEntryManager.
*/
public interface Callback {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupAlertTransferHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupAlertTransferHelper.java
index 2a68fa5..dd81c4e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupAlertTransferHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupAlertTransferHelper.java
@@ -29,7 +29,9 @@
import com.android.systemui.statusbar.InflationTask;
import com.android.systemui.statusbar.StatusBarStateController;
import com.android.systemui.statusbar.StatusBarStateController.StateListener;
+import com.android.systemui.statusbar.notification.AlertTransferListener;
import com.android.systemui.statusbar.notification.NotificationData.Entry;
+import com.android.systemui.statusbar.notification.NotificationEntryManager;
import com.android.systemui.statusbar.notification.row.NotificationInflater.AsyncInflationTask;
import com.android.systemui.statusbar.notification.row.NotificationInflater.InflationFlag;
import com.android.systemui.statusbar.phone.NotificationGroupManager.NotificationGroup;
@@ -38,8 +40,6 @@
import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashMap;
import java.util.Objects;
/**
@@ -47,8 +47,8 @@
* {@link HeadsUpManager}, {@link AmbientPulseManager}. In particular, this class deals with keeping
* the correct notification in a group alerting based off the group suppression.
*/
-public class NotificationGroupAlertTransferHelper implements OnGroupChangeListener,
- OnHeadsUpChangedListener, OnAmbientChangedListener, StateListener {
+public class NotificationGroupAlertTransferHelper implements OnHeadsUpChangedListener,
+ OnAmbientChangedListener, StateListener {
private static final long ALERT_TRANSFER_TIMEOUT = 300;
@@ -69,15 +69,7 @@
private final NotificationGroupManager mGroupManager =
Dependency.get(NotificationGroupManager.class);
- // TODO(b/119637830): It would be good if GroupManager already had all pending notifications as
- // normal children (i.e. add notifications to GroupManager before inflation) so that we don't
- // have to have this dependency. We'd also have to worry less about the suppression not being up
- // to date.
- /**
- * Notifications that are currently inflating for the first time. Used to remove an incorrectly
- * alerting notification faster.
- */
- private HashMap<String, Entry> mPendingNotifications;
+ private NotificationEntryManager mEntryManager;
private boolean mIsDozing;
@@ -85,6 +77,23 @@
Dependency.get(StatusBarStateController.class).addCallback(this);
}
+ /** Causes the TransferHelper to register itself as a listener to the appropriate classes. */
+ public void bind(NotificationEntryManager entryManager,
+ NotificationGroupManager groupManager) {
+ if (mEntryManager != null) {
+ throw new IllegalStateException("Already bound.");
+ }
+
+ // TODO(b/119637830): It would be good if GroupManager already had all pending notifications
+ // as normal children (i.e. add notifications to GroupManager before inflation) so that we
+ // don't have to have this dependency. We'd also have to worry less about the suppression
+ // not being up to date.
+ mEntryManager = entryManager;
+
+ mEntryManager.setAlertTransferListener(mAlertTransferListener);
+ groupManager.addOnGroupChangeListener(mOnGroupChangeListener);
+ }
+
/**
* Whether or not a notification has transferred its alert state to the notification and
* the notification should alert after inflating.
@@ -97,25 +106,10 @@
return alertInfo != null && alertInfo.isStillValid();
}
- /**
- * Removes any alerts pending on this entry. Note that this will not stop any inflation tasks
- * started by a transfer, so this should only be used as clean-up for when inflation is stopped
- * and the pending alert no longer needs to happen.
- *
- * @param key notification key that may have info that needs to be cleaned up
- */
- public void cleanUpPendingAlertInfo(@NonNull String key) {
- mPendingAlerts.remove(key);
- }
-
public void setHeadsUpManager(HeadsUpManager headsUpManager) {
mHeadsUpManager = headsUpManager;
}
- public void setPendingEntries(HashMap<String, Entry> pendingNotifications) {
- mPendingNotifications = pendingNotifications;
- }
-
@Override
public void onStateChanged(int newState) {}
@@ -130,43 +124,45 @@
mIsDozing = isDozing;
}
- @Override
- public void onGroupCreated(NotificationGroup group, String groupKey) {
- mGroupAlertEntries.put(groupKey, new GroupAlertEntry(group));
- }
+ private final OnGroupChangeListener mOnGroupChangeListener = new OnGroupChangeListener() {
+ @Override
+ public void onGroupCreated(NotificationGroup group, String groupKey) {
+ mGroupAlertEntries.put(groupKey, new GroupAlertEntry(group));
+ }
- @Override
- public void onGroupRemoved(NotificationGroup group, String groupKey) {
- mGroupAlertEntries.remove(groupKey);
- }
+ @Override
+ public void onGroupRemoved(NotificationGroup group, String groupKey) {
+ mGroupAlertEntries.remove(groupKey);
+ }
- @Override
- public void onGroupSuppressionChanged(NotificationGroup group, boolean suppressed) {
- AlertingNotificationManager alertManager = getActiveAlertManager();
- if (suppressed) {
- if (alertManager.isAlerting(group.summary.key)) {
- handleSuppressedSummaryAlerted(group.summary, alertManager);
- }
- } else {
- // Group summary can be null if we are no longer suppressed because the summary was
- // removed. In that case, we don't need to alert the summary.
- if (group.summary == null) {
- return;
- }
- GroupAlertEntry groupAlertEntry = mGroupAlertEntries.get(mGroupManager.getGroupKey(
- group.summary.notification));
- // Group is no longer suppressed. We should check if we need to transfer the alert
- // back to the summary now that it's no longer suppressed.
- if (groupAlertEntry.mAlertSummaryOnNextAddition) {
- if (!alertManager.isAlerting(group.summary.key)) {
- alertNotificationWhenPossible(group.summary, alertManager);
+ @Override
+ public void onGroupSuppressionChanged(NotificationGroup group, boolean suppressed) {
+ AlertingNotificationManager alertManager = getActiveAlertManager();
+ if (suppressed) {
+ if (alertManager.isAlerting(group.summary.key)) {
+ handleSuppressedSummaryAlerted(group.summary, alertManager);
}
- groupAlertEntry.mAlertSummaryOnNextAddition = false;
} else {
- checkShouldTransferBack(groupAlertEntry);
+ // Group summary can be null if we are no longer suppressed because the summary was
+ // removed. In that case, we don't need to alert the summary.
+ if (group.summary == null) {
+ return;
+ }
+ GroupAlertEntry groupAlertEntry = mGroupAlertEntries.get(mGroupManager.getGroupKey(
+ group.summary.notification));
+ // Group is no longer suppressed. We should check if we need to transfer the alert
+ // back to the summary now that it's no longer suppressed.
+ if (groupAlertEntry.mAlertSummaryOnNextAddition) {
+ if (!alertManager.isAlerting(group.summary.key)) {
+ alertNotificationWhenPossible(group.summary, alertManager);
+ }
+ groupAlertEntry.mAlertSummaryOnNextAddition = false;
+ } else {
+ checkShouldTransferBack(groupAlertEntry);
+ }
}
}
- }
+ };
@Override
public void onAmbientStateChanged(Entry entry, boolean isAmbient) {
@@ -185,37 +181,42 @@
}
}
- /**
- * Called when the entry's reinflation has finished. If there is an alert pending, we then
- * show the alert.
- *
- * @param entry entry whose inflation has finished
- */
- public void onInflationFinished(@NonNull Entry entry) {
- PendingAlertInfo alertInfo = mPendingAlerts.remove(entry.key);
- if (alertInfo != null) {
- if (alertInfo.isStillValid()) {
- alertNotificationWhenPossible(entry, getActiveAlertManager());
- } else {
- // The transfer is no longer valid. Free the content.
- entry.getRow().freeContentViewWhenSafe(alertInfo.mAlertManager.getContentFlag());
+ private final AlertTransferListener mAlertTransferListener = new AlertTransferListener() {
+ // Called when a new notification has been posted but is not inflated yet. We use this to
+ // see as early as we can if we need to abort a transfer.
+ @Override
+ public void onPendingEntryAdded(Entry entry) {
+ String groupKey = mGroupManager.getGroupKey(entry.notification);
+ GroupAlertEntry groupAlertEntry = mGroupAlertEntries.get(groupKey);
+ if (groupAlertEntry != null) {
+ checkShouldTransferBack(groupAlertEntry);
}
}
- }
- /**
- * Called when a new notification has been posted but is not inflated yet. We use this to see
- * as early as we can if we need to abort a transfer.
- *
- * @param entry entry that has been added
- */
- public void onPendingEntryAdded(@NonNull Entry entry) {
- String groupKey = mGroupManager.getGroupKey(entry.notification);
- GroupAlertEntry groupAlertEntry = mGroupAlertEntries.get(groupKey);
- if (groupAlertEntry != null) {
- checkShouldTransferBack(groupAlertEntry);
+ // Called when the entry's reinflation has finished. If there is an alert pending, we
+ // then show the alert.
+ @Override
+ public void onEntryReinflated(Entry entry) {
+ PendingAlertInfo alertInfo = mPendingAlerts.remove(entry.key);
+ if (alertInfo != null) {
+ if (alertInfo.isStillValid()) {
+ alertNotificationWhenPossible(entry, getActiveAlertManager());
+ } else {
+ // The transfer is no longer valid. Free the content.
+ entry.getRow().freeContentViewWhenSafe(
+ alertInfo.mAlertManager.getContentFlag());
+ }
+ }
}
- }
+
+ @Override
+ public void onEntryRemoved(Entry entry) {
+ // Removes any alerts pending on this entry. Note that this will not stop any inflation
+ // tasks started by a transfer, so this should only be used as clean-up for when
+ // inflation is stopped and the pending alert no longer needs to happen.
+ mPendingAlerts.remove(entry.key);
+ }
+ };
/**
* Gets the number of new notifications pending inflation that will be added to the group
@@ -225,11 +226,11 @@
* @return the number of new notifications that will be added to the group
*/
private int getPendingChildrenNotAlerting(@NonNull NotificationGroup group) {
- if (mPendingNotifications == null) {
+ if (mEntryManager == null) {
return 0;
}
int number = 0;
- Collection<Entry> values = mPendingNotifications.values();
+ Iterable<Entry> values = mEntryManager.getPendingNotificationsIterator();
for (Entry entry : values) {
if (isPendingNotificationInGroup(entry, group) && onlySummaryAlerts(entry)) {
number++;
@@ -245,10 +246,10 @@
* @return true if a pending notification will add to this group
*/
private boolean pendingInflationsWillAddChildren(@NonNull NotificationGroup group) {
- if (mPendingNotifications == null) {
+ if (mEntryManager == null) {
return false;
}
- Collection<Entry> values = mPendingNotifications.values();
+ Iterable<Entry> values = mEntryManager.getPendingNotificationsIterator();
for (Entry entry : values) {
if (isPendingNotificationInGroup(entry, group)) {
return true;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
index 1d6a1e8..a508f1b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
@@ -628,6 +628,8 @@
mBubbleController = Dependency.get(BubbleController.class);
mBubbleController.setExpandListener(mBubbleExpandListener);
+ mGroupAlertTransferHelper.bind(mEntryManager, mGroupManager);
+
mColorExtractor.addOnColorsChangedListener(this);
mStatusBarStateController.addCallback(this, StatusBarStateController.RANK_STATUS_BAR);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationGroupAlertTransferHelperTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationGroupAlertTransferHelperTest.java
index c3bc511..ee39e10 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationGroupAlertTransferHelperTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationGroupAlertTransferHelperTest.java
@@ -32,14 +32,19 @@
import com.android.systemui.SysuiTestCase;
import com.android.systemui.statusbar.AmbientPulseManager;
+import com.android.systemui.statusbar.notification.AlertTransferListener;
import com.android.systemui.statusbar.notification.NotificationData;
import com.android.systemui.statusbar.notification.NotificationData.Entry;
+import com.android.systemui.statusbar.notification.NotificationEntryManager;
import com.android.systemui.statusbar.policy.HeadsUpManager;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
@@ -49,13 +54,15 @@
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
public class NotificationGroupAlertTransferHelperTest extends SysuiTestCase {
- @Rule
- public MockitoRule rule = MockitoJUnit.rule();
+ @Rule public MockitoRule rule = MockitoJUnit.rule();
private NotificationGroupAlertTransferHelper mGroupAlertTransferHelper;
private NotificationGroupManager mGroupManager;
private AmbientPulseManager mAmbientPulseManager;
private HeadsUpManager mHeadsUpManager;
+ @Mock private NotificationEntryManager mNotificationEntryManager;
+ @Captor private ArgumentCaptor<AlertTransferListener> mListenerCaptor;
+ private AlertTransferListener mAlertTransferListener;
private final HashMap<String, Entry> mPendingEntries = new HashMap<>();
private final NotificationGroupTestHelper mGroupTestHelper =
new NotificationGroupTestHelper(mContext);
@@ -67,15 +74,19 @@
mDependency.injectTestDependency(AmbientPulseManager.class, mAmbientPulseManager);
mHeadsUpManager = new HeadsUpManager(mContext) {};
+ when(mNotificationEntryManager.getPendingNotificationsIterator())
+ .thenReturn(mPendingEntries.values());
+
mGroupManager = new NotificationGroupManager();
mDependency.injectTestDependency(NotificationGroupManager.class, mGroupManager);
mGroupManager.setHeadsUpManager(mHeadsUpManager);
mGroupAlertTransferHelper = new NotificationGroupAlertTransferHelper();
mGroupAlertTransferHelper.setHeadsUpManager(mHeadsUpManager);
- mGroupAlertTransferHelper.setPendingEntries(mPendingEntries);
- mGroupManager.addOnGroupChangeListener(mGroupAlertTransferHelper);
+ mGroupAlertTransferHelper.bind(mNotificationEntryManager, mGroupManager);
+ verify(mNotificationEntryManager).setAlertTransferListener(mListenerCaptor.capture());
+ mAlertTransferListener = mListenerCaptor.getValue();
mHeadsUpManager.addListener(mGroupAlertTransferHelper);
mAmbientPulseManager.addListener(mGroupAlertTransferHelper);
}
@@ -110,7 +121,7 @@
// Add second child notification so that summary is no longer suppressed.
mPendingEntries.put(childEntry2.key, childEntry2);
- mGroupAlertTransferHelper.onPendingEntryAdded(childEntry2);
+ mAlertTransferListener.onPendingEntryAdded(childEntry2);
mGroupManager.onEntryAdded(childEntry2);
// The alert state should transfer back to the summary as there is now more than one
@@ -137,7 +148,7 @@
// Add second child notification so that summary is no longer suppressed.
mPendingEntries.put(childEntry2.key, childEntry2);
- mGroupAlertTransferHelper.onPendingEntryAdded(childEntry2);
+ mAlertTransferListener.onPendingEntryAdded(childEntry2);
mGroupManager.onEntryAdded(childEntry2);
// Dozing changed so no reason to re-alert summary.
@@ -175,7 +186,7 @@
when(childEntry.getRow().isInflationFlagSet(mHeadsUpManager.getContentFlag()))
.thenReturn(true);
- mGroupAlertTransferHelper.onInflationFinished(childEntry);
+ mAlertTransferListener.onEntryReinflated(childEntry);
// Alert is immediately removed from summary, and we show child as its content is inflated.
assertFalse(mHeadsUpManager.isAlerting(summaryEntry.key));
@@ -199,13 +210,13 @@
// Add second child notification so that summary is no longer suppressed.
mPendingEntries.put(childEntry2.key, childEntry2);
- mGroupAlertTransferHelper.onPendingEntryAdded(childEntry2);
+ mAlertTransferListener.onPendingEntryAdded(childEntry2);
mGroupManager.onEntryAdded(childEntry2);
// Child entry finishes its inflation.
when(childEntry.getRow().isInflationFlagSet(mHeadsUpManager.getContentFlag()))
.thenReturn(true);
- mGroupAlertTransferHelper.onInflationFinished(childEntry);
+ mAlertTransferListener.onEntryReinflated(childEntry);
verify(childEntry.getRow(), times(1)).freeContentViewWhenSafe(mHeadsUpManager
.getContentFlag());
@@ -225,7 +236,7 @@
mGroupManager.onEntryAdded(summaryEntry);
mGroupManager.onEntryAdded(childEntry);
- mGroupAlertTransferHelper.cleanUpPendingAlertInfo(childEntry.key);
+ mAlertTransferListener.onEntryRemoved(childEntry);
assertFalse(mGroupAlertTransferHelper.isAlertTransferPending(childEntry));
}
diff --git a/services/core/java/com/android/server/LocationManagerService.java b/services/core/java/com/android/server/LocationManagerService.java
index 8b992eb..9dbfe5f 100644
--- a/services/core/java/com/android/server/LocationManagerService.java
+++ b/services/core/java/com/android/server/LocationManagerService.java
@@ -22,6 +22,7 @@
import static com.android.internal.util.Preconditions.checkState;
+import android.Manifest;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager;
@@ -189,6 +190,8 @@
private LocationBlacklist mBlacklist;
private GnssMeasurementsProvider mGnssMeasurementsProvider;
private GnssNavigationMessageProvider mGnssNavigationMessageProvider;
+ private String mLocationControllerExtraPackage;
+ private boolean mLocationControllerExtraPackageEnabled;
private IGpsGeofenceHardware mGpsGeofenceProxy;
// --- fields below are protected by mLock ---
@@ -2717,6 +2720,39 @@
return null;
}
+ @Override
+ public void setLocationControllerExtraPackage(String packageName) {
+ mContext.enforceCallingPermission(Manifest.permission.LOCATION_HARDWARE,
+ Manifest.permission.LOCATION_HARDWARE + " permission required");
+ synchronized (mLock) {
+ mLocationControllerExtraPackage = packageName;
+ }
+ }
+
+ @Override
+ public String getLocationControllerExtraPackage() {
+ synchronized (mLock) {
+ return mLocationControllerExtraPackage;
+ }
+ }
+
+ @Override
+ public void setLocationControllerExtraPackageEnabled(boolean enabled) {
+ mContext.enforceCallingPermission(Manifest.permission.LOCATION_HARDWARE,
+ Manifest.permission.LOCATION_HARDWARE + " permission required");
+ synchronized (mLock) {
+ mLocationControllerExtraPackageEnabled = enabled;
+ }
+ }
+
+ @Override
+ public boolean isLocationControllerExtraPackageEnabled() {
+ synchronized (mLock) {
+ return mLocationControllerExtraPackageEnabled
+ && (mLocationControllerExtraPackage != null);
+ }
+ }
+
/**
* Returns the current location enabled/disabled status for a user
*
@@ -3492,6 +3528,11 @@
}
}
+ if (mLocationControllerExtraPackage != null) {
+ pw.println(" Location controller extra package: " + mLocationControllerExtraPackage
+ + " enabled: " + mLocationControllerExtraPackageEnabled);
+ }
+
if (!mBackgroundThrottlePackageWhitelist.isEmpty()) {
pw.println(" Throttling Whitelisted Packages:");
for (String packageName : mBackgroundThrottlePackageWhitelist) {
diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java
index b04ae17..b6c4921 100644
--- a/services/core/java/com/android/server/TelephonyRegistry.java
+++ b/services/core/java/com/android/server/TelephonyRegistry.java
@@ -1996,6 +1996,13 @@
android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE, null);
}
+ if ((events & PhoneStateListener.LISTEN_PREFERRED_DATA_SUBID_CHANGE) != 0) {
+ // It can have either READ_PHONE_STATE or READ_PRIVILEGED_PHONE_STATE.
+ TelephonyPermissions.checkReadPhoneState(mContext,
+ SubscriptionManager.INVALID_SUBSCRIPTION_ID, Binder.getCallingPid(),
+ Binder.getCallingUid(), callingPackage, "listen to "
+ + "LISTEN_PREFERRED_DATA_SUBID_CHANGE");
+ }
return true;
}
diff --git a/services/core/java/com/android/server/job/JobSchedulerService.java b/services/core/java/com/android/server/job/JobSchedulerService.java
index 1325f04..10dc156 100644
--- a/services/core/java/com/android/server/job/JobSchedulerService.java
+++ b/services/core/java/com/android/server/job/JobSchedulerService.java
@@ -67,6 +67,7 @@
import android.os.Temperature;
import android.os.UserHandle;
import android.os.UserManagerInternal;
+import android.os.WorkSource;
import android.provider.Settings;
import android.text.format.DateUtils;
import android.util.KeyValueListParser;
@@ -364,6 +365,8 @@
private static final String KEY_CONN_CONGESTION_DELAY_FRAC = "conn_congestion_delay_frac";
private static final String KEY_CONN_PREFETCH_RELAX_FRAC = "conn_prefetch_relax_frac";
private static final String KEY_USE_HEARTBEATS = "use_heartbeats";
+ private static final String KEY_TIME_CONTROLLER_SKIP_NOT_READY_JOBS =
+ "tc_skip_not_ready_jobs";
private static final String KEY_QUOTA_CONTROLLER_ALLOWED_TIME_PER_PERIOD_MS =
"qc_allowed_time_per_period_ms";
private static final String KEY_QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS =
@@ -376,6 +379,8 @@
"qc_window_size_frequent_ms";
private static final String KEY_QUOTA_CONTROLLER_WINDOW_SIZE_RARE_MS =
"qc_window_size_rare_ms";
+ private static final String KEY_QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS =
+ "qc_max_execution_time_ms";
private static final int DEFAULT_MIN_IDLE_COUNT = 1;
private static final int DEFAULT_MIN_CHARGING_COUNT = 1;
@@ -402,6 +407,7 @@
private static final float DEFAULT_CONN_CONGESTION_DELAY_FRAC = 0.5f;
private static final float DEFAULT_CONN_PREFETCH_RELAX_FRAC = 0.5f;
private static final boolean DEFAULT_USE_HEARTBEATS = true;
+ private static final boolean DEFAULT_TIME_CONTROLLER_SKIP_NOT_READY_JOBS = false;
private static final long DEFAULT_QUOTA_CONTROLLER_ALLOWED_TIME_PER_PERIOD_MS =
10 * 60 * 1000L; // 10 minutes
private static final long DEFAULT_QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS =
@@ -414,6 +420,8 @@
8 * 60 * 60 * 1000L; // 8 hours
private static final long DEFAULT_QUOTA_CONTROLLER_WINDOW_SIZE_RARE_MS =
24 * 60 * 60 * 1000L; // 24 hours
+ private static final long DEFAULT_QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS =
+ 4 * 60 * 60 * 1000L; // 4 hours
/**
* Minimum # of idle jobs that must be ready in order to force the JMS to schedule things
@@ -538,6 +546,13 @@
*/
public boolean USE_HEARTBEATS = DEFAULT_USE_HEARTBEATS;
+ /**
+ * Whether or not TimeController should skip setting wakeup alarms for jobs that aren't
+ * ready now.
+ */
+ public boolean TIME_CONTROLLER_SKIP_NOT_READY_JOBS =
+ DEFAULT_TIME_CONTROLLER_SKIP_NOT_READY_JOBS;
+
/** How much time each app will have to run jobs within their standby bucket window. */
public long QUOTA_CONTROLLER_ALLOWED_TIME_PER_PERIOD_MS =
DEFAULT_QUOTA_CONTROLLER_ALLOWED_TIME_PER_PERIOD_MS;
@@ -581,6 +596,12 @@
public long QUOTA_CONTROLLER_WINDOW_SIZE_RARE_MS =
DEFAULT_QUOTA_CONTROLLER_WINDOW_SIZE_RARE_MS;
+ /**
+ * The maximum amount of time an app can have its jobs running within a 24 hour window.
+ */
+ public long QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS =
+ DEFAULT_QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS;
+
private final KeyValueListParser mParser = new KeyValueListParser(',');
void updateConstantsLocked(String value) {
@@ -653,6 +674,9 @@
CONN_PREFETCH_RELAX_FRAC = mParser.getFloat(KEY_CONN_PREFETCH_RELAX_FRAC,
DEFAULT_CONN_PREFETCH_RELAX_FRAC);
USE_HEARTBEATS = mParser.getBoolean(KEY_USE_HEARTBEATS, DEFAULT_USE_HEARTBEATS);
+ TIME_CONTROLLER_SKIP_NOT_READY_JOBS = mParser.getBoolean(
+ KEY_TIME_CONTROLLER_SKIP_NOT_READY_JOBS,
+ DEFAULT_TIME_CONTROLLER_SKIP_NOT_READY_JOBS);
QUOTA_CONTROLLER_ALLOWED_TIME_PER_PERIOD_MS = mParser.getDurationMillis(
KEY_QUOTA_CONTROLLER_ALLOWED_TIME_PER_PERIOD_MS,
DEFAULT_QUOTA_CONTROLLER_ALLOWED_TIME_PER_PERIOD_MS);
@@ -671,6 +695,9 @@
QUOTA_CONTROLLER_WINDOW_SIZE_RARE_MS = mParser.getDurationMillis(
KEY_QUOTA_CONTROLLER_WINDOW_SIZE_RARE_MS,
DEFAULT_QUOTA_CONTROLLER_WINDOW_SIZE_RARE_MS);
+ QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS = mParser.getDurationMillis(
+ KEY_QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS,
+ DEFAULT_QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS);
}
void dump(IndentingPrintWriter pw) {
@@ -705,6 +732,8 @@
pw.printPair(KEY_CONN_CONGESTION_DELAY_FRAC, CONN_CONGESTION_DELAY_FRAC).println();
pw.printPair(KEY_CONN_PREFETCH_RELAX_FRAC, CONN_PREFETCH_RELAX_FRAC).println();
pw.printPair(KEY_USE_HEARTBEATS, USE_HEARTBEATS).println();
+ pw.printPair(KEY_TIME_CONTROLLER_SKIP_NOT_READY_JOBS,
+ TIME_CONTROLLER_SKIP_NOT_READY_JOBS).println();
pw.printPair(KEY_QUOTA_CONTROLLER_ALLOWED_TIME_PER_PERIOD_MS,
QUOTA_CONTROLLER_ALLOWED_TIME_PER_PERIOD_MS).println();
pw.printPair(KEY_QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS,
@@ -717,6 +746,8 @@
QUOTA_CONTROLLER_WINDOW_SIZE_FREQUENT_MS).println();
pw.printPair(KEY_QUOTA_CONTROLLER_WINDOW_SIZE_RARE_MS,
QUOTA_CONTROLLER_WINDOW_SIZE_RARE_MS).println();
+ pw.printPair(KEY_QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS,
+ QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS).println();
pw.decreaseIndent();
}
@@ -748,6 +779,11 @@
proto.write(ConstantsProto.CONN_PREFETCH_RELAX_FRAC, CONN_PREFETCH_RELAX_FRAC);
proto.write(ConstantsProto.USE_HEARTBEATS, USE_HEARTBEATS);
+ final long tcToken = proto.start(ConstantsProto.TIME_CONTROLLER);
+ proto.write(ConstantsProto.TimeController.SKIP_NOT_READY_JOBS,
+ TIME_CONTROLLER_SKIP_NOT_READY_JOBS);
+ proto.end(tcToken);
+
final long qcToken = proto.start(ConstantsProto.QUOTA_CONTROLLER);
proto.write(ConstantsProto.QuotaController.ALLOWED_TIME_PER_PERIOD_MS,
QUOTA_CONTROLLER_ALLOWED_TIME_PER_PERIOD_MS);
@@ -761,6 +797,8 @@
QUOTA_CONTROLLER_WINDOW_SIZE_FREQUENT_MS);
proto.write(ConstantsProto.QuotaController.RARE_WINDOW_SIZE_MS,
QUOTA_CONTROLLER_WINDOW_SIZE_RARE_MS);
+ proto.write(ConstantsProto.QuotaController.MAX_EXECUTION_TIME_MS,
+ QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS);
proto.end(qcToken);
proto.end(token);
@@ -952,9 +990,15 @@
return mConstants;
}
+ public boolean isChainedAttributionEnabled() {
+ return WorkSource.isChainedBatteryAttributionEnabled(getContext());
+ }
+
@Override
public void onStartUser(int userHandle) {
- mStartedUsers = ArrayUtils.appendInt(mStartedUsers, userHandle);
+ synchronized (mLock) {
+ mStartedUsers = ArrayUtils.appendInt(mStartedUsers, userHandle);
+ }
// Let's kick any outstanding jobs for this user.
mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget();
}
@@ -967,7 +1011,9 @@
@Override
public void onStopUser(int userHandle) {
- mStartedUsers = ArrayUtils.removeInt(mStartedUsers, userHandle);
+ synchronized (mLock) {
+ mStartedUsers = ArrayUtils.removeInt(mStartedUsers, userHandle);
+ }
}
/**
@@ -2150,8 +2196,7 @@
final boolean jobExists = mJobs.containsJob(job);
- final int userId = job.getUserId();
- final boolean userStarted = ArrayUtils.contains(mStartedUsers, userId);
+ final boolean userStarted = areUsersStartedLocked(job);
if (DEBUG) {
Slog.v(TAG, "isReadyToBeExecutedLocked: " + job.toShortString()
@@ -2239,7 +2284,7 @@
try {
componentPresent = (AppGlobals.getPackageManager().getServiceInfo(
job.getServiceComponent(), PackageManager.MATCH_DEBUG_TRIAGED_MISSING,
- userId) != null);
+ job.getUserId()) != null);
} catch (RemoteException e) {
throw e.rethrowAsRuntimeException();
}
@@ -3052,6 +3097,13 @@
printed = true;
pw.println("user-stopped");
}
+ if (!ArrayUtils.contains(mStartedUsers, js.getSourceUserId())) {
+ if (printed) {
+ pw.print(" ");
+ }
+ printed = true;
+ pw.println("source-user-stopped");
+ }
if (mBackingUpUids.indexOfKey(js.getSourceUid()) >= 0) {
if (printed) {
pw.print(" ");
@@ -3203,7 +3255,7 @@
pw.print(" (job=");
pw.print(job.isReady());
pw.print(" user=");
- pw.print(ArrayUtils.contains(mStartedUsers, job.getUserId()));
+ pw.print(areUsersStartedLocked(job));
pw.print(" !pending=");
pw.print(!mPendingJobs.contains(job));
pw.print(" !active=");
@@ -3373,7 +3425,7 @@
proto.write(JobSchedulerServiceDumpProto.RegisteredJob.IS_JOB_READY,
job.isReady());
proto.write(JobSchedulerServiceDumpProto.RegisteredJob.IS_USER_STARTED,
- ArrayUtils.contains(mStartedUsers, job.getUserId()));
+ areUsersStartedLocked(job));
proto.write(JobSchedulerServiceDumpProto.RegisteredJob.IS_JOB_PENDING,
mPendingJobs.contains(job));
proto.write(JobSchedulerServiceDumpProto.RegisteredJob.IS_JOB_CURRENTLY_ACTIVE,
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 58ee217..ac2dbdf 100644
--- a/services/core/java/com/android/server/job/controllers/QuotaController.java
+++ b/services/core/java/com/android/server/job/controllers/QuotaController.java
@@ -54,14 +54,13 @@
import com.android.server.job.StateControllerProto;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Predicate;
/**
- * Controller that tracks whether a package has exceeded its standby bucket quota.
+ * 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,
@@ -203,6 +202,80 @@
}
}
+ private static int hashLong(long val) {
+ return (int) (val ^ (val >>> 32));
+ }
+
+ @VisibleForTesting
+ static class ExecutionStats {
+ /**
+ * The time at which this record should be considered invalid, in the elapsed realtime
+ * timebase.
+ */
+ public long invalidTimeElapsed;
+
+ public long windowSizeMs;
+
+ /** The total amount of time the app ran in its respective bucket window size. */
+ public long executionTimeInWindowMs;
+ public int bgJobCountInWindow;
+
+ /** The total amount of time the app ran in the last {@link MAX_PERIOD_MS}. */
+ public long executionTimeInMaxPeriodMs;
+ public int bgJobCountInMaxPeriod;
+
+ /**
+ * 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}.
+ */
+ public long quotaCutoffTimeElapsed;
+
+ @Override
+ public String toString() {
+ return new StringBuilder()
+ .append("invalidTime=").append(invalidTimeElapsed).append(", ")
+ .append("windowSize=").append(windowSizeMs).append(", ")
+ .append("executionTimeInWindow=").append(executionTimeInWindowMs).append(", ")
+ .append("bgJobCountInWindow=").append(bgJobCountInWindow).append(", ")
+ .append("executionTimeInMaxPeriod=").append(executionTimeInMaxPeriodMs)
+ .append(", ")
+ .append("bgJobCountInMaxPeriod=").append(bgJobCountInMaxPeriod).append(", ")
+ .append("quotaCutoffTime=").append(quotaCutoffTimeElapsed)
+ .toString();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof ExecutionStats) {
+ ExecutionStats other = (ExecutionStats) obj;
+ return this.invalidTimeElapsed == other.invalidTimeElapsed
+ && this.windowSizeMs == other.windowSizeMs
+ && this.executionTimeInWindowMs == other.executionTimeInWindowMs
+ && this.bgJobCountInWindow == other.bgJobCountInWindow
+ && this.executionTimeInMaxPeriodMs == other.executionTimeInMaxPeriodMs
+ && this.bgJobCountInMaxPeriod == other.bgJobCountInMaxPeriod
+ && this.quotaCutoffTimeElapsed == other.quotaCutoffTimeElapsed;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 0;
+ result = 31 * result + hashLong(invalidTimeElapsed);
+ result = 31 * result + hashLong(windowSizeMs);
+ result = 31 * result + hashLong(executionTimeInWindowMs);
+ result = 31 * result + bgJobCountInWindow;
+ result = 31 * result + hashLong(executionTimeInMaxPeriodMs);
+ result = 31 * result + bgJobCountInMaxPeriod;
+ result = 31 * result + hashLong(quotaCutoffTimeElapsed);
+ return result;
+ }
+ }
+
/** List of all tracked jobs keyed by source package-userId combo. */
private final UserPackageMap<ArraySet<JobStatus>> mTrackedJobs = new UserPackageMap<>();
@@ -218,6 +291,9 @@
*/
private final UserPackageMap<QcAlarmListener> mInQuotaAlarmListeners = new UserPackageMap<>();
+ /** Cached calculation results for each app, with the standby buckets as the array indices. */
+ private final UserPackageMap<ExecutionStats[]> mExecutionStatsCache = new UserPackageMap<>();
+
private final AlarmManager mAlarmManager;
private final ChargingTracker mChargeTracker;
private final Handler mHandler;
@@ -235,11 +311,29 @@
private long mAllowedTimePerPeriodMs = 10 * MINUTE_IN_MILLIS;
/**
- * How much time the package should have before transitioning from out-of-quota to in-quota.
- * This should not affect processing if the package is already in-quota.
+ * 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;
+
+ /**
+ * 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
+ /**
+ * {@link mAllowedTimePerPeriodMs} - {@link mQuotaBufferMs}. This can be used to determine when
+ * an app will have enough quota to transition from out-of-quota to in-quota.
+ */
+ private long mAllowedTimeIntoQuotaMs = mAllowedTimePerPeriodMs - mQuotaBufferMs;
+
+ /**
+ * {@link mMaxExecutionTimeMs} - {@link mQuotaBufferMs}. This can be used to determine when an
+ * app will have enough quota to transition from out-of-quota to in-quota.
+ */
+ private long mMaxExecutionTimeIntoQuotaMs = mMaxExecutionTimeMs - mQuotaBufferMs;
+
private long mNextCleanupTimeElapsed = 0;
private final AlarmManager.OnAlarmListener mSessionCleanupAlarmListener =
new AlarmManager.OnAlarmListener() {
@@ -263,7 +357,7 @@
/** The maximum period any bucket can have. */
private static final long MAX_PERIOD_MS = 24 * 60 * MINUTE_IN_MILLIS;
- /** A package has reached its quota. The message should contain a {@link Package} object. */
+ /** An app has reached its quota. The message should contain a {@link Package} object. */
private static final int MSG_REACHED_QUOTA = 0;
/** Drop any old timing sessions. */
private static final int MSG_CLEAN_UP_SESSIONS = 1;
@@ -341,12 +435,15 @@
Math.max(MINUTE_IN_MILLIS, mConstants.QUOTA_CONTROLLER_ALLOWED_TIME_PER_PERIOD_MS));
if (mAllowedTimePerPeriodMs != newAllowedTimeMs) {
mAllowedTimePerPeriodMs = newAllowedTimeMs;
+ mAllowedTimeIntoQuotaMs = mAllowedTimePerPeriodMs - mQuotaBufferMs;
changed = true;
}
long newQuotaBufferMs = Math.max(0,
Math.min(5 * MINUTE_IN_MILLIS, mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS));
if (mQuotaBufferMs != newQuotaBufferMs) {
mQuotaBufferMs = newQuotaBufferMs;
+ mAllowedTimeIntoQuotaMs = mAllowedTimePerPeriodMs - mQuotaBufferMs;
+ mMaxExecutionTimeIntoQuotaMs = mMaxExecutionTimeMs - mQuotaBufferMs;
changed = true;
}
long newActivePeriodMs = Math.max(mAllowedTimePerPeriodMs,
@@ -373,6 +470,13 @@
mBucketPeriodsMs[RARE_INDEX] = newRarePeriodMs;
changed = true;
}
+ long newMaxExecutionTimeMs = Math.max(60 * MINUTE_IN_MILLIS,
+ Math.min(MAX_PERIOD_MS, mConstants.QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS));
+ if (mMaxExecutionTimeMs != newMaxExecutionTimeMs) {
+ mMaxExecutionTimeMs = newMaxExecutionTimeMs;
+ mMaxExecutionTimeIntoQuotaMs = mMaxExecutionTimeMs - mQuotaBufferMs;
+ changed = true;
+ }
if (changed) {
// Update job bookkeeping out of band.
@@ -406,6 +510,7 @@
mAlarmManager.cancel(alarmListener);
mInQuotaAlarmListeners.delete(userId, packageName);
}
+ mExecutionStatsCache.delete(userId, packageName);
}
@Override
@@ -414,6 +519,7 @@
mPkgTimers.delete(userId);
mTimingSessions.delete(userId);
mInQuotaAlarmListeners.delete(userId);
+ mExecutionStatsCache.delete(userId);
}
/**
@@ -439,7 +545,6 @@
private boolean isWithinQuotaLocked(final int userId, @NonNull final String packageName,
final int standbyBucket) {
if (standbyBucket == NEVER_INDEX) return false;
- if (standbyBucket == ACTIVE_INDEX) return true;
// This check is needed in case the flag is toggled after a job has been registered.
if (!mShouldThrottle) return true;
@@ -472,46 +577,152 @@
if (standbyBucket == NEVER_INDEX) {
return 0;
}
- final long bucketWindowSizeMs = mBucketPeriodsMs[standbyBucket];
- final long trailingRunDurationMs = getTrailingExecutionTimeLocked(
- userId, packageName, bucketWindowSizeMs);
- return mAllowedTimePerPeriodMs - trailingRunDurationMs;
+ final ExecutionStats stats = getExecutionStatsLocked(userId, packageName, standbyBucket);
+ return Math.min(mAllowedTimePerPeriodMs - stats.executionTimeInWindowMs,
+ mMaxExecutionTimeMs - stats.executionTimeInMaxPeriodMs);
}
- /** Returns how long the uid has had jobs running within the most recent window. */
+ /** Returns the execution stats of the app in the most recent window. */
@VisibleForTesting
- long getTrailingExecutionTimeLocked(final int userId, @NonNull final String packageName,
- final long windowSizeMs) {
- long totalTime = 0;
+ @NonNull
+ ExecutionStats getExecutionStatsLocked(final int userId, @NonNull final String packageName,
+ final int standbyBucket) {
+ if (standbyBucket == NEVER_INDEX) {
+ Slog.wtf(TAG, "getExecutionStatsLocked called for a NEVER app.");
+ return new ExecutionStats();
+ }
+ ExecutionStats[] appStats = mExecutionStatsCache.get(userId, packageName);
+ if (appStats == null) {
+ appStats = new ExecutionStats[mBucketPeriodsMs.length];
+ mExecutionStatsCache.add(userId, packageName, appStats);
+ }
+ ExecutionStats stats = appStats[standbyBucket];
+ if (stats == null) {
+ stats = new ExecutionStats();
+ appStats[standbyBucket] = stats;
+ }
+ final long bucketWindowSizeMs = mBucketPeriodsMs[standbyBucket];
+ Timer timer = mPkgTimers.get(userId, packageName);
+ if ((timer != null && timer.isActive())
+ || stats.invalidTimeElapsed <= sElapsedRealtimeClock.millis()
+ || stats.windowSizeMs != bucketWindowSizeMs) {
+ // The stats are no longer valid.
+ stats.windowSizeMs = bucketWindowSizeMs;
+ updateExecutionStatsLocked(userId, packageName, stats);
+ }
+
+ return stats;
+ }
+
+ @VisibleForTesting
+ void updateExecutionStatsLocked(final int userId, @NonNull final String packageName,
+ @NonNull ExecutionStats stats) {
+ stats.executionTimeInWindowMs = 0;
+ stats.bgJobCountInWindow = 0;
+ stats.executionTimeInMaxPeriodMs = 0;
+ stats.bgJobCountInMaxPeriod = 0;
+ stats.quotaCutoffTimeElapsed = 0;
Timer timer = mPkgTimers.get(userId, packageName);
final long nowElapsed = sElapsedRealtimeClock.millis();
+ stats.invalidTimeElapsed = nowElapsed + MAX_PERIOD_MS;
if (timer != null && timer.isActive()) {
- totalTime = timer.getCurrentDuration(nowElapsed);
+ stats.executionTimeInWindowMs =
+ stats.executionTimeInMaxPeriodMs = timer.getCurrentDuration(nowElapsed);
+ stats.bgJobCountInWindow = stats.bgJobCountInMaxPeriod = timer.getBgJobCount();
+ // If the timer is active, the value will be stale at the next method call, so
+ // invalidate now.
+ stats.invalidTimeElapsed = nowElapsed;
+ if (stats.executionTimeInWindowMs >= mAllowedTimeIntoQuotaMs) {
+ stats.quotaCutoffTimeElapsed = Math.max(stats.quotaCutoffTimeElapsed,
+ nowElapsed - mAllowedTimeIntoQuotaMs);
+ }
+ if (stats.executionTimeInMaxPeriodMs >= mMaxExecutionTimeIntoQuotaMs) {
+ stats.quotaCutoffTimeElapsed = Math.max(stats.quotaCutoffTimeElapsed,
+ nowElapsed - mMaxExecutionTimeIntoQuotaMs);
+ }
}
List<TimingSession> sessions = mTimingSessions.get(userId, packageName);
if (sessions == null || sessions.size() == 0) {
- return totalTime;
+ return;
}
- final long startElapsed = nowElapsed - windowSizeMs;
+ final long startWindowElapsed = nowElapsed - stats.windowSizeMs;
+ final long startMaxElapsed = nowElapsed - MAX_PERIOD_MS;
+ // The minimum time between the start time and the beginning of the sessions that were
+ // looked at --> how much time the stats will be valid for.
+ long emptyTimeMs = Long.MAX_VALUE;
// Sessions are non-overlapping and in order of occurrence, so iterating backwards will get
// the most recent ones.
for (int i = sessions.size() - 1; i >= 0; --i) {
TimingSession session = sessions.get(i);
- if (startElapsed < session.startTimeElapsed) {
- totalTime += session.endTimeElapsed - session.startTimeElapsed;
- } else if (startElapsed < session.endTimeElapsed) {
+
+ // Window management.
+ if (startWindowElapsed < session.startTimeElapsed) {
+ stats.executionTimeInWindowMs += session.endTimeElapsed - session.startTimeElapsed;
+ 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);
+ }
+ } 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.
- totalTime += session.endTimeElapsed - startElapsed;
+ 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);
+ }
+ }
+
+ // Max period check.
+ if (startMaxElapsed < session.startTimeElapsed) {
+ stats.executionTimeInMaxPeriodMs +=
+ session.endTimeElapsed - session.startTimeElapsed;
+ stats.bgJobCountInMaxPeriod += session.bgJobCount;
+ emptyTimeMs = Math.min(emptyTimeMs, session.startTimeElapsed - startMaxElapsed);
+ if (stats.executionTimeInMaxPeriodMs >= mMaxExecutionTimeIntoQuotaMs) {
+ stats.quotaCutoffTimeElapsed = Math.max(stats.quotaCutoffTimeElapsed,
+ session.startTimeElapsed + stats.executionTimeInMaxPeriodMs
+ - mMaxExecutionTimeIntoQuotaMs);
+ }
+ } else if (startMaxElapsed < session.endTimeElapsed) {
+ // The session started before the window but ended within the window. Only include
+ // the portion that was within the window.
+ stats.executionTimeInMaxPeriodMs += session.endTimeElapsed - startMaxElapsed;
+ stats.bgJobCountInMaxPeriod += session.bgJobCount;
+ emptyTimeMs = 0;
+ if (stats.executionTimeInMaxPeriodMs >= mMaxExecutionTimeIntoQuotaMs) {
+ stats.quotaCutoffTimeElapsed = Math.max(stats.quotaCutoffTimeElapsed,
+ startMaxElapsed + stats.executionTimeInMaxPeriodMs
+ - mMaxExecutionTimeIntoQuotaMs);
+ }
} else {
// This session ended before the window. No point in going any further.
- return totalTime;
+ break;
}
}
- return totalTime;
+ stats.invalidTimeElapsed = nowElapsed + emptyTimeMs;
+ }
+
+ private void invalidateAllExecutionStatsLocked(final int userId,
+ @NonNull final String packageName) {
+ ExecutionStats[] appStats = mExecutionStatsCache.get(userId, packageName);
+ if (appStats != null) {
+ final long nowElapsed = sElapsedRealtimeClock.millis();
+ for (int i = 0; i < appStats.length; ++i) {
+ ExecutionStats stats = appStats[i];
+ if (stats != null) {
+ stats.invalidTimeElapsed = nowElapsed;
+ }
+ }
+ }
}
@VisibleForTesting
@@ -524,6 +735,8 @@
mTimingSessions.add(userId, packageName, sessions);
}
sessions.add(session);
+ // Adding a new session means that the current stats are now incorrect.
+ invalidateAllExecutionStatsLocked(userId, packageName);
maybeScheduleCleanupAlarmLocked();
}
@@ -657,87 +870,43 @@
@VisibleForTesting
void maybeScheduleStartAlarmLocked(final int userId, @NonNull final String packageName,
final int standbyBucket) {
- final String pkgString = string(userId, packageName);
if (standbyBucket == NEVER_INDEX) {
return;
- } else if (standbyBucket == ACTIVE_INDEX) {
- // ACTIVE apps are "always" in quota.
- if (DEBUG) {
- Slog.w(TAG, "maybeScheduleStartAlarmLocked called for " + pkgString
- + " even though it is active");
- }
- mHandler.obtainMessage(MSG_CHECK_PACKAGE, userId, 0, packageName).sendToTarget();
+ }
- QcAlarmListener alarmListener = mInQuotaAlarmListeners.get(userId, packageName);
+ final String pkgString = string(userId, packageName);
+ ExecutionStats stats = getExecutionStatsLocked(userId, packageName, standbyBucket);
+ QcAlarmListener alarmListener = mInQuotaAlarmListeners.get(userId, packageName);
+ if (stats.executionTimeInWindowMs < mAllowedTimePerPeriodMs
+ && stats.executionTimeInMaxPeriodMs < mMaxExecutionTimeMs) {
+ // Already in quota. Why was this method called?
+ if (DEBUG) {
+ Slog.e(TAG, "maybeScheduleStartAlarmLocked called for " + pkgString
+ + " even though it already has "
+ + getRemainingExecutionTimeLocked(userId, packageName, standbyBucket)
+ + "ms in its quota.");
+ }
if (alarmListener != null) {
// Cancel any pending alarm.
mAlarmManager.cancel(alarmListener);
// Set the trigger time to 0 so that the alarm doesn't think it's still waiting.
alarmListener.setTriggerTime(0);
}
- return;
- }
-
- List<TimingSession> sessions = mTimingSessions.get(userId, packageName);
- if (sessions == null || sessions.size() == 0) {
- // If there are no sessions, then the job is probably in quota.
- if (DEBUG) {
- Slog.wtf(TAG, "maybeScheduleStartAlarmLocked called for " + pkgString
- + " even though it is likely within its quota.");
- }
mHandler.obtainMessage(MSG_CHECK_PACKAGE, userId, 0, packageName).sendToTarget();
return;
}
-
- final long bucketWindowSizeMs = mBucketPeriodsMs[standbyBucket];
- final long nowElapsed = sElapsedRealtimeClock.millis();
- // How far back we need to look.
- final long startElapsed = nowElapsed - bucketWindowSizeMs;
-
- long totalTime = 0;
- long cutoffTimeElapsed = nowElapsed;
- for (int i = sessions.size() - 1; i >= 0; i--) {
- TimingSession session = sessions.get(i);
- if (startElapsed < session.startTimeElapsed) {
- cutoffTimeElapsed = session.startTimeElapsed;
- totalTime += session.endTimeElapsed - session.startTimeElapsed;
- } else if (startElapsed < session.endTimeElapsed) {
- // The session started before the window but ended within the window. Only
- // include the portion that was within the window.
- cutoffTimeElapsed = startElapsed;
- totalTime += session.endTimeElapsed - startElapsed;
- } else {
- // This session ended before the window. No point in going any further.
- break;
- }
- if (totalTime >= mAllowedTimePerPeriodMs) {
- break;
- }
- }
- if (totalTime < mAllowedTimePerPeriodMs) {
- // Already in quota. Why was this method called?
- if (DEBUG) {
- Slog.w(TAG, "maybeScheduleStartAlarmLocked called for " + pkgString
- + " even though it already has " + (mAllowedTimePerPeriodMs - totalTime)
- + "ms in its quota.");
- }
- mHandler.obtainMessage(MSG_CHECK_PACKAGE, userId, 0, packageName).sendToTarget();
- return;
- }
-
- QcAlarmListener alarmListener = mInQuotaAlarmListeners.get(userId, packageName);
if (alarmListener == null) {
alarmListener = new QcAlarmListener(userId, packageName);
mInQuotaAlarmListeners.add(userId, packageName, alarmListener);
}
- // We add all the way back to the beginning of a session (or the window) even when we don't
- // need to (in order to simplify the for loop above), so there might be some extra we
- // need to add back.
- final long extraTimeMs = totalTime - mAllowedTimePerPeriodMs;
// The time this app will have quota again.
- final long inQuotaTimeElapsed =
- cutoffTimeElapsed + extraTimeMs + mQuotaBufferMs + bucketWindowSizeMs;
+ long inQuotaTimeElapsed =
+ stats.quotaCutoffTimeElapsed + stats.windowSizeMs;
+ if (stats.executionTimeInMaxPeriodMs >= mMaxExecutionTimeMs) {
+ inQuotaTimeElapsed = Math.max(inQuotaTimeElapsed,
+ stats.quotaCutoffTimeElapsed + MAX_PERIOD_MS);
+ }
// Only schedule the alarm if:
// 1. There isn't one currently scheduled
// 2. The new alarm is significantly earlier than the previous alarm (which could be the
@@ -747,13 +916,15 @@
// TODO: this might be overengineering. Simplify if proven safe.
if (!alarmListener.isWaiting()
|| inQuotaTimeElapsed < alarmListener.getTriggerTimeElapsed() - 3 * MINUTE_IN_MILLIS
- || alarmListener.getTriggerTimeElapsed() < inQuotaTimeElapsed - mQuotaBufferMs) {
+ || alarmListener.getTriggerTimeElapsed() < inQuotaTimeElapsed) {
if (DEBUG) Slog.d(TAG, "Scheduling start alarm for " + pkgString);
// If the next time this app will have quota is at least 3 minutes before the
// alarm is supposed to go off, reschedule the alarm.
mAlarmManager.set(AlarmManager.ELAPSED_REALTIME, inQuotaTimeElapsed,
ALARM_TAG_QUOTA_CHECK, alarmListener, mHandler);
alarmListener.setTriggerTime(inQuotaTimeElapsed);
+ } else if (DEBUG) {
+ Slog.d(TAG, "No need to scheduling start alarm for " + pkgString);
}
}
@@ -816,10 +987,18 @@
// How many background jobs ran during this session.
public final int bgJobCount;
- TimingSession(long startElapsed, long endElapsed, int jobCount) {
+ private final int mHashCode;
+
+ TimingSession(long startElapsed, long endElapsed, int bgJobCount) {
this.startTimeElapsed = startElapsed;
this.endTimeElapsed = endElapsed;
- this.bgJobCount = jobCount;
+ this.bgJobCount = bgJobCount;
+
+ int hashCode = 0;
+ hashCode = 31 * hashCode + hashLong(startTimeElapsed);
+ hashCode = 31 * hashCode + hashLong(endTimeElapsed);
+ hashCode = 31 * hashCode + bgJobCount;
+ mHashCode = hashCode;
}
@Override
@@ -842,7 +1021,7 @@
@Override
public int hashCode() {
- return Arrays.hashCode(new long[] {startTimeElapsed, endTimeElapsed, bgJobCount});
+ return mHashCode;
}
public void dump(IndentingPrintWriter pw) {
@@ -902,6 +1081,9 @@
if (mRunningBgJobs.size() == 1) {
// Started tracking the first job.
mStartTimeElapsed = sElapsedRealtimeClock.millis();
+ // Starting the timer means that all cached execution stats are now
+ // incorrect.
+ invalidateAllExecutionStatsLocked(mPkg.userId, mPkg.packageName);
scheduleCutoff();
}
}
@@ -966,6 +1148,12 @@
}
}
+ int getBgJobCount() {
+ synchronized (mLock) {
+ return mBgJobCount;
+ }
+ }
+
void onChargingChanged(long nowElapsed, boolean isCharging) {
synchronized (mLock) {
if (isCharging) {
@@ -978,6 +1166,9 @@
// repeatedly plugged in and unplugged, the job count for a package may be
// artificially high.
mBgJobCount = mRunningBgJobs.size();
+ // Starting the timer means that all cached execution stats are now
+ // incorrect.
+ invalidateAllExecutionStatsLocked(mPkg.userId, mPkg.packageName);
// Schedule cutoff since we're now actively tracking for quotas again.
scheduleCutoff();
}
@@ -1239,6 +1430,11 @@
}
@VisibleForTesting
+ long getMaxExecutionTimeMs() {
+ return mMaxExecutionTimeMs;
+ }
+
+ @VisibleForTesting
@Nullable
List<TimingSession> getTimingSessions(int userId, String packageName) {
return mTimingSessions.get(userId, packageName);
diff --git a/services/core/java/com/android/server/job/controllers/TimeController.java b/services/core/java/com/android/server/job/controllers/TimeController.java
index 04d5795..26f3caf 100644
--- a/services/core/java/com/android/server/job/controllers/TimeController.java
+++ b/services/core/java/com/android/server/job/controllers/TimeController.java
@@ -30,6 +30,7 @@
import android.util.TimeUtils;
import android.util.proto.ProtoOutputStream;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
import com.android.server.job.JobSchedulerService;
import com.android.server.job.StateControllerProto;
@@ -68,7 +69,7 @@
mNextJobExpiredElapsedMillis = Long.MAX_VALUE;
mNextDelayExpiredElapsedMillis = Long.MAX_VALUE;
- mChainedAttributionEnabled = WorkSource.isChainedBatteryAttributionEnabled(mContext);
+ mChainedAttributionEnabled = mService.isChainedAttributionEnabled();
}
/**
@@ -110,11 +111,24 @@
it.next();
}
it.add(job);
+
job.setTrackingController(JobStatus.TRACKING_TIME);
- maybeUpdateAlarmsLocked(
- job.hasTimingDelayConstraint() ? job.getEarliestRunTime() : Long.MAX_VALUE,
- job.hasDeadlineConstraint() ? job.getLatestRunTimeElapsed() : Long.MAX_VALUE,
- deriveWorkSource(job.getSourceUid(), job.getSourcePackageName()));
+ WorkSource ws = deriveWorkSource(job.getSourceUid(), job.getSourcePackageName());
+ final long deadlineExpiredElapsed =
+ job.hasDeadlineConstraint() ? job.getLatestRunTimeElapsed() : Long.MAX_VALUE;
+ final long delayExpiredElapsed =
+ job.hasTimingDelayConstraint() ? job.getEarliestRunTime() : Long.MAX_VALUE;
+ if (mConstants.TIME_CONTROLLER_SKIP_NOT_READY_JOBS) {
+ if (wouldBeReadyWithConstraintLocked(job, JobStatus.CONSTRAINT_TIMING_DELAY)) {
+ maybeUpdateDelayAlarmLocked(delayExpiredElapsed, ws);
+ }
+ if (wouldBeReadyWithConstraintLocked(job, JobStatus.CONSTRAINT_DEADLINE)) {
+ maybeUpdateDeadlineAlarmLocked(deadlineExpiredElapsed, ws);
+ }
+ } else {
+ maybeUpdateDelayAlarmLocked(delayExpiredElapsed, ws);
+ maybeUpdateDeadlineAlarmLocked(deadlineExpiredElapsed, ws);
+ }
}
}
@@ -133,6 +147,34 @@
}
}
+ @Override
+ public void onConstantsUpdatedLocked() {
+ checkExpiredDelaysAndResetAlarm();
+ checkExpiredDeadlinesAndResetAlarm();
+ }
+
+ @Override
+ public void evaluateStateLocked(JobStatus job) {
+ if (!mConstants.TIME_CONTROLLER_SKIP_NOT_READY_JOBS) {
+ return;
+ }
+
+ if (job.hasTimingDelayConstraint()
+ && job.getEarliestRunTime() <= mNextDelayExpiredElapsedMillis) {
+ checkExpiredDelaysAndResetAlarm();
+ }
+ if (job.hasDeadlineConstraint()
+ && job.getLatestRunTimeElapsed() <= mNextJobExpiredElapsedMillis) {
+ checkExpiredDeadlinesAndResetAlarm();
+ }
+ }
+
+ @Override
+ public void reevaluateStateLocked(int uid) {
+ checkExpiredDelaysAndResetAlarm();
+ checkExpiredDeadlinesAndResetAlarm();
+ }
+
/**
* Determines whether this controller can stop tracking the given job.
* The controller is no longer interested in a job once its time constraint is satisfied, and
@@ -156,14 +198,15 @@
* Checks list of jobs for ones that have an expired deadline, sending them to the JobScheduler
* if so, removing them from this list, and updating the alarm for the next expiry time.
*/
- private void checkExpiredDeadlinesAndResetAlarm() {
+ @VisibleForTesting
+ void checkExpiredDeadlinesAndResetAlarm() {
synchronized (mLock) {
long nextExpiryTime = Long.MAX_VALUE;
int nextExpiryUid = 0;
String nextExpiryPackageName = null;
final long nowElapsedMillis = sElapsedRealtimeClock.millis();
- Iterator<JobStatus> it = mTrackedJobs.iterator();
+ ListIterator<JobStatus> it = mTrackedJobs.listIterator();
while (it.hasNext()) {
JobStatus job = it.next();
if (!job.hasDeadlineConstraint()) {
@@ -171,9 +214,22 @@
}
if (evaluateDeadlineConstraint(job, nowElapsedMillis)) {
- mStateChangedListener.onRunJobNow(job);
+ if (job.isReady()) {
+ // If the job still isn't ready, there's no point trying to rush the
+ // Scheduler.
+ mStateChangedListener.onRunJobNow(job);
+ }
it.remove();
} else { // Sorted by expiry time, so take the next one and stop.
+ if (mConstants.TIME_CONTROLLER_SKIP_NOT_READY_JOBS
+ && !wouldBeReadyWithConstraintLocked(
+ job, JobStatus.CONSTRAINT_DEADLINE)) {
+ if (DEBUG) {
+ Slog.i(TAG,
+ "Skipping " + job + " because deadline won't make it ready.");
+ }
+ continue;
+ }
nextExpiryTime = job.getLatestRunTimeElapsed();
nextExpiryUid = job.getSourceUid();
nextExpiryPackageName = job.getSourcePackageName();
@@ -202,7 +258,8 @@
* Handles alarm that notifies us that a job's delay has expired. Iterates through the list of
* tracked jobs and marks them as ready as appropriate.
*/
- private void checkExpiredDelaysAndResetAlarm() {
+ @VisibleForTesting
+ void checkExpiredDelaysAndResetAlarm() {
synchronized (mLock) {
final long nowElapsedMillis = sElapsedRealtimeClock.millis();
long nextDelayTime = Long.MAX_VALUE;
@@ -223,6 +280,15 @@
ready = true;
}
} else if (!job.isConstraintSatisfied(JobStatus.CONSTRAINT_TIMING_DELAY)) {
+ if (mConstants.TIME_CONTROLLER_SKIP_NOT_READY_JOBS
+ && !wouldBeReadyWithConstraintLocked(
+ job, JobStatus.CONSTRAINT_TIMING_DELAY)) {
+ if (DEBUG) {
+ Slog.i(TAG,
+ "Skipping " + job + " because delay won't make it ready.");
+ }
+ continue;
+ }
// If this job still doesn't have its delay constraint satisfied,
// then see if it is the next upcoming delay time for the alarm.
final long jobDelayTime = job.getEarliestRunTime();
@@ -262,11 +328,13 @@
return false;
}
- private void maybeUpdateAlarmsLocked(long delayExpiredElapsed, long deadlineExpiredElapsed,
- WorkSource ws) {
+ private void maybeUpdateDelayAlarmLocked(long delayExpiredElapsed, WorkSource ws) {
if (delayExpiredElapsed < mNextDelayExpiredElapsedMillis) {
setDelayExpiredAlarmLocked(delayExpiredElapsed, ws);
}
+ }
+
+ private void maybeUpdateDeadlineAlarmLocked(long deadlineExpiredElapsed, WorkSource ws) {
if (deadlineExpiredElapsed < mNextJobExpiredElapsedMillis) {
setDeadlineExpiredAlarmLocked(deadlineExpiredElapsed, ws);
}
@@ -297,11 +365,7 @@
}
private long maybeAdjustAlarmTime(long proposedAlarmTimeElapsedMillis) {
- final long earliestWakeupTimeElapsed = sElapsedRealtimeClock.millis();
- if (proposedAlarmTimeElapsedMillis < earliestWakeupTimeElapsed) {
- return earliestWakeupTimeElapsed;
- }
- return proposedAlarmTimeElapsedMillis;
+ return Math.max(proposedAlarmTimeElapsedMillis, sElapsedRealtimeClock.millis());
}
private void updateAlarmWithListenerLocked(String tag, OnAlarmListener listener,
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 6cfb846..ae29180 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -1064,9 +1064,6 @@
+ verificationId + " packageName:" + packageName);
return;
}
- if (DEBUG_DOMAIN_VERIFICATION) Slog.d(TAG,
- "Updating IntentFilterVerificationInfo for package " + packageName
- +" verificationId:" + verificationId);
synchronized (mPackages) {
if (verified) {
@@ -1084,19 +1081,47 @@
int updatedStatus = INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_UNDEFINED;
boolean needUpdate = false;
- // We cannot override the STATUS_ALWAYS / STATUS_NEVER states if they have
- // already been set by the User thru the Disambiguation dialog
+ if (DEBUG_DOMAIN_VERIFICATION) {
+ Slog.d(TAG,
+ "Updating IntentFilterVerificationInfo for package " + packageName
+ + " verificationId:" + verificationId
+ + " verified=" + verified);
+ }
+
+ // In a success case, we promote from undefined or ASK to ALWAYS. This
+ // supports a flow where the app fails validation but then ships an updated
+ // APK that passes, and therefore deserves to be in ALWAYS.
+ //
+ // If validation failed, the undefined state winds up in the basic ASK behavior,
+ // but apps that previously passed and became ALWAYS are *demoted* out of
+ // that state, since they would not deserve the ALWAYS behavior in case of a
+ // clean install.
switch (userStatus) {
+ case INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ALWAYS:
+ if (!verified) {
+ // updatedStatus is already UNDEFINED
+ needUpdate = true;
+
+ if (DEBUG_DOMAIN_VERIFICATION) {
+ Slog.d(TAG, "Formerly validated but now failing; demoting");
+ }
+ }
+ break;
+
case INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_UNDEFINED:
+ // Stay in 'undefined' on verification failure
if (verified) {
updatedStatus = INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ALWAYS;
- } else {
- updatedStatus = INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ASK;
}
needUpdate = true;
+ if (DEBUG_DOMAIN_VERIFICATION) {
+ Slog.d(TAG, "Applying update; old=" + userStatus
+ + " new=" + updatedStatus);
+ }
break;
case INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ASK:
+ // Keep in 'ask' on failure
if (verified) {
updatedStatus = INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ALWAYS;
needUpdate = true;
@@ -1112,6 +1137,8 @@
packageName, updatedStatus, userId);
scheduleWritePackageRestrictionsLocked(userId);
}
+ } else {
+ Slog.i(TAG, "autoVerify ignored when installing for all users");
}
}
}
@@ -15269,7 +15296,7 @@
| (killApp ? 0 : PackageManager.DELETE_DONT_KILL_APP);
deletePackageAction = mayDeletePackageLocked(res.removedInfo,
prepareResult.originalPs, prepareResult.disabledPs,
- prepareResult.childPackageSettings, deleteFlags, installArgs.user);
+ prepareResult.childPackageSettings, deleteFlags, null /* all users */);
if (deletePackageAction == null) {
throw new ReconcileFailure(
PackageManager.INSTALL_FAILED_REPLACE_COULDNT_DELETE,
@@ -16708,6 +16735,7 @@
int status = ivi.getStatus();
switch (status) {
case INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_UNDEFINED:
+ case INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ALWAYS:
case INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ASK:
return true;
@@ -17356,28 +17384,22 @@
* make sure this flag is set for partially installed apps. If not its meaningless to
* delete a partially installed application.
*/
- private void removePackageDataLIF(PackageSetting ps, int[] allUserHandles,
+ private void removePackageDataLIF(final PackageSetting deletedPs, int[] allUserHandles,
PackageRemovedInfo outInfo, int flags, boolean writeSettings) {
- String packageName = ps.name;
- if (DEBUG_REMOVE) Slog.d(TAG, "removePackageDataLI: " + ps);
+ String packageName = deletedPs.name;
+ if (DEBUG_REMOVE) Slog.d(TAG, "removePackageDataLI: " + deletedPs);
// Retrieve object to delete permissions for shared user later on
- final PackageParser.Package deletedPkg;
- final PackageSetting deletedPs;
- // reader
- synchronized (mPackages) {
- deletedPkg = mPackages.get(packageName);
- deletedPs = mSettings.mPackages.get(packageName);
- if (outInfo != null) {
- outInfo.removedPackage = packageName;
- outInfo.installerPackageName = ps.installerPackageName;
- outInfo.isStaticSharedLib = deletedPkg != null
- && deletedPkg.staticSharedLibName != null;
- outInfo.populateUsers(deletedPs == null ? null
- : deletedPs.queryInstalledUsers(sUserManager.getUserIds(), true), deletedPs);
- }
+ final PackageParser.Package deletedPkg = deletedPs.pkg;
+ if (outInfo != null) {
+ outInfo.removedPackage = packageName;
+ outInfo.installerPackageName = deletedPs.installerPackageName;
+ outInfo.isStaticSharedLib = deletedPkg != null
+ && deletedPkg.staticSharedLibName != null;
+ outInfo.populateUsers(deletedPs == null ? null
+ : deletedPs.queryInstalledUsers(sUserManager.getUserIds(), true), deletedPs);
}
- removePackageLI(ps.name, (flags & PackageManager.DELETE_CHATTY) != 0);
+ removePackageLI(deletedPs.name, (flags & PackageManager.DELETE_CHATTY) != 0);
if ((flags & PackageManager.DELETE_KEEP_DATA) == 0) {
final PackageParser.Package resolvedPkg;
@@ -17386,8 +17408,8 @@
} else {
// We don't have a parsed package when it lives on an ejected
// adopted storage device, so fake something together
- resolvedPkg = new PackageParser.Package(ps.name);
- resolvedPkg.setVolumeUuid(ps.volumeUuid);
+ resolvedPkg = new PackageParser.Package(deletedPs.name);
+ resolvedPkg.setVolumeUuid(deletedPs.volumeUuid);
}
destroyAppDataLIF(resolvedPkg, UserHandle.USER_ALL,
StorageManager.FLAG_STORAGE_DE | StorageManager.FLAG_STORAGE_CE);
@@ -17447,10 +17469,10 @@
if (DEBUG_REMOVE) {
Slog.d(TAG, " user " + userId + " => " + installed);
}
- if (installed != ps.getInstalled(userId)) {
+ if (installed != deletedPs.getInstalled(userId)) {
installedStateChanged = true;
}
- ps.setInstalled(installed, userId);
+ deletedPs.setInstalled(installed, userId);
}
}
}
@@ -17460,7 +17482,7 @@
mSettings.writeLPr();
}
if (installedStateChanged) {
- mSettings.writeKernelMappingLPr(ps);
+ mSettings.writeKernelMappingLPr(deletedPs);
}
}
if (removedAppId != -1) {
@@ -17530,12 +17552,12 @@
/*
* Tries to delete system package.
*/
- private void deleteSystemPackageLIF(DeletePackageAction action,
- PackageParser.Package deletedPkg, PackageSetting deletedPs, int[] allUserHandles,
- int flags, PackageRemovedInfo outInfo, boolean writeSettings)
+ private void deleteSystemPackageLIF(DeletePackageAction action, PackageSetting deletedPs,
+ int[] allUserHandles, int flags, PackageRemovedInfo outInfo, boolean writeSettings)
throws SystemDeleteException {
final boolean applyUserRestrictions
= (allUserHandles != null) && (outInfo.origUsers != null);
+ final PackageParser.Package deletedPkg = deletedPs.pkg;
// Confirm if the system package has been updated
// An updated system app can be deleted. This will also have to restore
// the system pkg from system partition
@@ -17895,16 +17917,18 @@
PackageSetting[] children = mSettings.getChildSettingsLPr(ps);
action = mayDeletePackageLocked(outInfo, ps, disabledPs, children, flags, user);
}
+ if (DEBUG_REMOVE) Slog.d(TAG, "deletePackageLI: " + packageName + " user " + user);
if (null == action) {
+ if (DEBUG_REMOVE) Slog.d(TAG, "deletePackageLI: action was null");
return false;
}
- if (DEBUG_REMOVE) Slog.d(TAG, "deletePackageLI: " + packageName + " user " + user);
try {
executeDeletePackageLIF(action, packageName, deleteCodeAndResources,
allUserHandles, writeSettings, replacingPackage);
} catch (SystemDeleteException e) {
+ if (DEBUG_REMOVE) Slog.d(TAG, "deletePackageLI: system deletion failure", e);
return false;
}
return true;
@@ -17951,42 +17975,48 @@
unsuspendForSuspendingPackage(packageName, userId);
}
-
if (!systemApp || action.mayDeleteUnupdatedSystemApp) {
// The caller is asking that the package only be deleted for a single
// user. To do this, we just mark its uninstalled state and delete
// its data. If this is a system app, we only allow this to happen if
// they have set the special DELETE_SYSTEM_APP which requests different
// semantics than normal for uninstalling system apps.
- markPackageUninstalledForUserLPw(ps, user);
-
- if (!systemApp) {
- // Do not uninstall the APK if an app should be cached
- boolean keepUninstalledPackage = shouldKeepUninstalledPackageLPr(packageName);
- if (ps.isAnyInstalled(sUserManager.getUserIds()) || keepUninstalledPackage) {
- // Other user still have this package installed, so all
+ synchronized (mPackages) {
+ markPackageUninstalledForUserLPw(ps, user);
+ if (!systemApp) {
+ // Do not uninstall the APK if an app should be cached
+ boolean keepUninstalledPackage = shouldKeepUninstalledPackageLPr(packageName);
+ if (ps.isAnyInstalled(sUserManager.getUserIds()) || keepUninstalledPackage) {
+ // Other users still have this package installed, so all
+ // we need to do is clear this user's data and save that
+ // it is uninstalled.
+ if (DEBUG_REMOVE) Slog.d(TAG, "Still installed by other users");
+ clearPackageStateForUserLIF(ps, userId, outInfo, flags);
+ scheduleWritePackageRestrictionsLocked(user);
+ return;
+ } else {
+ // We need to set it back to 'installed' so the uninstall
+ // broadcasts will be sent correctly.
+ if (DEBUG_REMOVE) Slog.d(TAG, "Not installed by other users, full delete");
+ if (userId != UserHandle.USER_ALL) {
+ ps.setInstalled(true, userId);
+ } else {
+ for (int origUserId : outInfo.origUsers) {
+ ps.setInstalled(true, origUserId);
+ }
+ }
+ mSettings.writeKernelMappingLPr(ps);
+ }
+ } else {
+ // This is a system app, so we assume that the
+ // other users still have this package installed, so all
// we need to do is clear this user's data and save that
// it is uninstalled.
- if (DEBUG_REMOVE) Slog.d(TAG, "Still installed by other users");
- clearPackageStateForUserLIF(ps, user.getIdentifier(), outInfo, flags);
+ if (DEBUG_REMOVE) Slog.d(TAG, "Deleting system app");
+ clearPackageStateForUserLIF(ps, userId, outInfo, flags);
scheduleWritePackageRestrictionsLocked(user);
return;
- } else {
- // We need to set it back to 'installed' so the uninstall
- // broadcasts will be sent correctly.
- if (DEBUG_REMOVE) Slog.d(TAG, "Not installed by other users, full delete");
- ps.setInstalled(true, user.getIdentifier());
- mSettings.writeKernelMappingLPr(ps);
}
- } else {
- // This is a system app, so we assume that the
- // other users still have this package installed, so all
- // we need to do is clear this user's data and save that
- // it is uninstalled.
- if (DEBUG_REMOVE) Slog.d(TAG, "Deleting system app");
- clearPackageStateForUserLIF(ps, user.getIdentifier(), outInfo, flags);
- scheduleWritePackageRestrictionsLocked(user);
- return;
}
}
@@ -18015,8 +18045,7 @@
if (DEBUG_REMOVE) Slog.d(TAG, "Removing system package: " + ps.name);
// When an updated system application is deleted we delete the existing resources
// as well and fall back to existing code in system partition
- deleteSystemPackageLIF(
- action, ps.pkg, ps, allUserHandles, flags, outInfo, writeSettings);
+ deleteSystemPackageLIF(action, ps, allUserHandles, flags, outInfo, writeSettings);
} else {
if (DEBUG_REMOVE) Slog.d(TAG, "Removing non-system package: " + ps.name);
deleteInstalledPackageLIF(ps, deleteCodeAndResources, flags, allUserHandles,
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 effb5a7..f1cd0cd 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
@@ -30,6 +30,7 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
@@ -62,6 +63,7 @@
import com.android.server.LocalServices;
import com.android.server.job.JobSchedulerService;
import com.android.server.job.JobSchedulerService.Constants;
+import com.android.server.job.controllers.QuotaController.ExecutionStats;
import com.android.server.job.controllers.QuotaController.TimingSession;
import org.junit.After;
@@ -131,13 +133,18 @@
doReturn(mock(PackageManagerInternal.class))
.when(() -> LocalServices.getService(PackageManagerInternal.class));
- // Freeze the clocks at this moment in time
+ // Freeze the clocks at 24 hours after this moment in time. Several tests create sessions
+ // in the past, and QuotaController sometimes floors values at 0, so if the test time
+ // causes sessions with negative timestamps, they will fail.
JobSchedulerService.sSystemClock =
- Clock.fixed(Clock.systemUTC().instant(), ZoneOffset.UTC);
- JobSchedulerService.sUptimeMillisClock =
- Clock.fixed(SystemClock.uptimeMillisClock().instant(), ZoneOffset.UTC);
- JobSchedulerService.sElapsedRealtimeClock =
- Clock.fixed(SystemClock.elapsedRealtimeClock().instant(), ZoneOffset.UTC);
+ getAdvancedClock(Clock.fixed(Clock.systemUTC().instant(), ZoneOffset.UTC),
+ 24 * HOUR_IN_MILLIS);
+ JobSchedulerService.sUptimeMillisClock = getAdvancedClock(
+ Clock.fixed(SystemClock.uptimeMillisClock().instant(), ZoneOffset.UTC),
+ 24 * HOUR_IN_MILLIS);
+ JobSchedulerService.sElapsedRealtimeClock = getAdvancedClock(
+ Clock.fixed(SystemClock.elapsedRealtimeClock().instant(), ZoneOffset.UTC),
+ 24 * HOUR_IN_MILLIS);
// Initialize real objects.
// Capture the listeners.
@@ -291,9 +298,17 @@
mQuotaController.saveTimingSession(0, "com.android.test.stay", two);
mQuotaController.saveTimingSession(0, "com.android.test.stay", one);
+ ExecutionStats expectedStats = new ExecutionStats();
+ expectedStats.invalidTimeElapsed = now + 24 * HOUR_IN_MILLIS;
+ expectedStats.windowSizeMs = 24 * HOUR_IN_MILLIS;
+
mQuotaController.onAppRemovedLocked("com.android.test.remove", 10001);
assertNull(mQuotaController.getTimingSessions(0, "com.android.test.remove"));
assertEquals(expected, mQuotaController.getTimingSessions(0, "com.android.test.stay"));
+ assertEquals(expectedStats,
+ mQuotaController.getExecutionStatsLocked(0, "com.android.test.remove", RARE_INDEX));
+ assertNotEquals(expectedStats,
+ mQuotaController.getExecutionStatsLocked(0, "com.android.test.stay", RARE_INDEX));
}
@Test
@@ -318,13 +333,21 @@
mQuotaController.saveTimingSession(10, "com.android.test", two);
mQuotaController.saveTimingSession(10, "com.android.test", one);
+ ExecutionStats expectedStats = new ExecutionStats();
+ expectedStats.invalidTimeElapsed = now + 24 * HOUR_IN_MILLIS;
+ expectedStats.windowSizeMs = 24 * HOUR_IN_MILLIS;
+
mQuotaController.onUserRemovedLocked(0);
assertNull(mQuotaController.getTimingSessions(0, "com.android.test"));
assertEquals(expected, mQuotaController.getTimingSessions(10, "com.android.test"));
+ assertEquals(expectedStats,
+ mQuotaController.getExecutionStatsLocked(0, "com.android.test", RARE_INDEX));
+ assertNotEquals(expectedStats,
+ mQuotaController.getExecutionStatsLocked(10, "com.android.test", RARE_INDEX));
}
@Test
- public void testGetTrailingExecutionTimeLocked_NoTimer() {
+ public void testUpdateExecutionStatsLocked_NoTimer() {
final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
// Added in chronological order.
mQuotaController.saveTimingSession(0, "com.android.test",
@@ -340,32 +363,288 @@
mQuotaController.saveTimingSession(0, "com.android.test",
createTimingSession(now - 5 * MINUTE_IN_MILLIS, 4 * MINUTE_IN_MILLIS, 3));
- assertEquals(0, mQuotaController.getTrailingExecutionTimeLocked(0, "com.android.test",
- MINUTE_IN_MILLIS));
- assertEquals(2 * MINUTE_IN_MILLIS,
- mQuotaController.getTrailingExecutionTimeLocked(0, "com.android.test",
- 3 * MINUTE_IN_MILLIS));
- assertEquals(4 * MINUTE_IN_MILLIS,
- mQuotaController.getTrailingExecutionTimeLocked(0, "com.android.test",
- 5 * MINUTE_IN_MILLIS));
- assertEquals(4 * MINUTE_IN_MILLIS,
- mQuotaController.getTrailingExecutionTimeLocked(0, "com.android.test",
- 49 * MINUTE_IN_MILLIS));
- assertEquals(5 * MINUTE_IN_MILLIS,
- mQuotaController.getTrailingExecutionTimeLocked(0, "com.android.test",
- 50 * MINUTE_IN_MILLIS));
- assertEquals(6 * MINUTE_IN_MILLIS,
- mQuotaController.getTrailingExecutionTimeLocked(0, "com.android.test",
- HOUR_IN_MILLIS));
- assertEquals(11 * MINUTE_IN_MILLIS,
- mQuotaController.getTrailingExecutionTimeLocked(0, "com.android.test",
- 2 * HOUR_IN_MILLIS));
- assertEquals(12 * MINUTE_IN_MILLIS,
- mQuotaController.getTrailingExecutionTimeLocked(0, "com.android.test",
- 3 * HOUR_IN_MILLIS));
- assertEquals(22 * MINUTE_IN_MILLIS,
- mQuotaController.getTrailingExecutionTimeLocked(0, "com.android.test",
- 6 * HOUR_IN_MILLIS));
+ // Test an app that hasn't had any activity.
+ ExecutionStats expectedStats = new ExecutionStats();
+ ExecutionStats inputStats = new ExecutionStats();
+
+ inputStats.windowSizeMs = expectedStats.windowSizeMs = 12 * HOUR_IN_MILLIS;
+ // Invalid time is now +24 hours since there are no sessions at all for the app.
+ expectedStats.invalidTimeElapsed = now + 24 * HOUR_IN_MILLIS;
+ mQuotaController.updateExecutionStatsLocked(0, "com.android.test.not.run", inputStats);
+ assertEquals(expectedStats, inputStats);
+
+ inputStats.windowSizeMs = expectedStats.windowSizeMs = MINUTE_IN_MILLIS;
+ // Invalid time is now +18 hours since there are no sessions in the window but the earliest
+ // session is 6 hours ago.
+ expectedStats.invalidTimeElapsed = now + 18 * HOUR_IN_MILLIS;
+ expectedStats.executionTimeInWindowMs = 0;
+ expectedStats.bgJobCountInWindow = 0;
+ expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInMaxPeriod = 15;
+ mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats);
+ assertEquals(expectedStats, inputStats);
+
+ inputStats.windowSizeMs = expectedStats.windowSizeMs = 3 * MINUTE_IN_MILLIS;
+ // Invalid time is now since the session straddles the window cutoff time.
+ expectedStats.invalidTimeElapsed = now;
+ expectedStats.executionTimeInWindowMs = 2 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInWindow = 3;
+ expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInMaxPeriod = 15;
+ mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats);
+ assertEquals(expectedStats, inputStats);
+
+ inputStats.windowSizeMs = expectedStats.windowSizeMs = 5 * MINUTE_IN_MILLIS;
+ // Invalid time is now since the start of the session is at the very edge of the window
+ // cutoff time.
+ expectedStats.invalidTimeElapsed = now;
+ expectedStats.executionTimeInWindowMs = 4 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInWindow = 3;
+ expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInMaxPeriod = 15;
+ mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats);
+ assertEquals(expectedStats, inputStats);
+
+ inputStats.windowSizeMs = expectedStats.windowSizeMs = 49 * MINUTE_IN_MILLIS;
+ // Invalid time is now +44 minutes since the earliest session in the window is now-5
+ // minutes.
+ expectedStats.invalidTimeElapsed = now + 44 * MINUTE_IN_MILLIS;
+ expectedStats.executionTimeInWindowMs = 4 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInWindow = 3;
+ expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInMaxPeriod = 15;
+ mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats);
+ assertEquals(expectedStats, inputStats);
+
+ inputStats.windowSizeMs = expectedStats.windowSizeMs = 50 * MINUTE_IN_MILLIS;
+ // Invalid time is now since the session is at the very edge of the window cutoff time.
+ expectedStats.invalidTimeElapsed = now;
+ expectedStats.executionTimeInWindowMs = 5 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInWindow = 4;
+ expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInMaxPeriod = 15;
+ mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats);
+ assertEquals(expectedStats, inputStats);
+
+ inputStats.windowSizeMs = expectedStats.windowSizeMs = HOUR_IN_MILLIS;
+ // Invalid time is now since the start of the session is at the very edge of the window
+ // cutoff time.
+ expectedStats.invalidTimeElapsed = now;
+ expectedStats.executionTimeInWindowMs = 6 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInWindow = 5;
+ expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInMaxPeriod = 15;
+ mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats);
+ assertEquals(expectedStats, inputStats);
+
+ inputStats.windowSizeMs = expectedStats.windowSizeMs = 2 * HOUR_IN_MILLIS;
+ // Invalid time is now since the session straddles the window cutoff time.
+ expectedStats.invalidTimeElapsed = now;
+ expectedStats.executionTimeInWindowMs = 11 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInWindow = 10;
+ expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInMaxPeriod = 15;
+ expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - MINUTE_IN_MILLIS)
+ + mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS;
+ mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats);
+ assertEquals(expectedStats, inputStats);
+
+ inputStats.windowSizeMs = expectedStats.windowSizeMs = 3 * HOUR_IN_MILLIS;
+ // Invalid time is now +59 minutes since the earliest session in the window is now-121
+ // minutes.
+ expectedStats.invalidTimeElapsed = now + 59 * MINUTE_IN_MILLIS;
+ expectedStats.executionTimeInWindowMs = 12 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInWindow = 10;
+ expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInMaxPeriod = 15;
+ expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - MINUTE_IN_MILLIS)
+ + mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS;
+ mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats);
+ assertEquals(expectedStats, inputStats);
+
+ inputStats.windowSizeMs = expectedStats.windowSizeMs = 6 * HOUR_IN_MILLIS;
+ // Invalid time is now since the start of the session is at the very edge of the window
+ // cutoff time.
+ expectedStats.invalidTimeElapsed = now;
+ expectedStats.executionTimeInWindowMs = 22 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInWindow = 15;
+ expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInMaxPeriod = 15;
+ expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - MINUTE_IN_MILLIS)
+ + mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS;
+ mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats);
+ assertEquals(expectedStats, inputStats);
+
+ // Make sure invalidTimeElapsed is set correctly when it's dependent on the max period.
+ mQuotaController.getTimingSessions(0, "com.android.test")
+ .add(0,
+ createTimingSession(now - (23 * HOUR_IN_MILLIS), MINUTE_IN_MILLIS, 3));
+ inputStats.windowSizeMs = expectedStats.windowSizeMs = 8 * HOUR_IN_MILLIS;
+ // 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.invalidTimeElapsed = now + HOUR_IN_MILLIS;
+ expectedStats.executionTimeInWindowMs = 22 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInWindow = 15;
+ expectedStats.executionTimeInMaxPeriodMs = 23 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInMaxPeriod = 18;
+ expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - MINUTE_IN_MILLIS)
+ + mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS;
+ mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats);
+ assertEquals(expectedStats, inputStats);
+
+ mQuotaController.getTimingSessions(0, "com.android.test")
+ .add(0,
+ createTimingSession(now - (24 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS),
+ 2 * MINUTE_IN_MILLIS, 2));
+ inputStats.windowSizeMs = expectedStats.windowSizeMs = 8 * HOUR_IN_MILLIS;
+ // Invalid time is now since the earlist session straddles the max period cutoff time.
+ expectedStats.invalidTimeElapsed = now;
+ expectedStats.executionTimeInWindowMs = 22 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInWindow = 15;
+ expectedStats.executionTimeInMaxPeriodMs = 24 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInMaxPeriod = 20;
+ expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - MINUTE_IN_MILLIS)
+ + mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS;
+ mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats);
+ assertEquals(expectedStats, inputStats);
+ }
+
+ /**
+ * Tests that getExecutionStatsLocked returns the correct stats.
+ */
+ @Test
+ public void testGetExecutionStatsLocked_Values() {
+ final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+ mQuotaController.saveTimingSession(0, "com.android.test",
+ createTimingSession(now - (23 * HOUR_IN_MILLIS), 10 * MINUTE_IN_MILLIS, 5));
+ mQuotaController.saveTimingSession(0, "com.android.test",
+ createTimingSession(now - (7 * HOUR_IN_MILLIS), 10 * MINUTE_IN_MILLIS, 5));
+ mQuotaController.saveTimingSession(0, "com.android.test",
+ createTimingSession(now - (2 * HOUR_IN_MILLIS), 10 * MINUTE_IN_MILLIS, 5));
+ mQuotaController.saveTimingSession(0, "com.android.test",
+ createTimingSession(now - (6 * MINUTE_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5));
+
+ ExecutionStats expectedStats = new ExecutionStats();
+
+ // Active
+ expectedStats.windowSizeMs = 10 * MINUTE_IN_MILLIS;
+ expectedStats.invalidTimeElapsed = now + 4 * MINUTE_IN_MILLIS;
+ expectedStats.executionTimeInWindowMs = 3 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInWindow = 5;
+ expectedStats.executionTimeInMaxPeriodMs = 33 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInMaxPeriod = 20;
+ assertEquals(expectedStats,
+ mQuotaController.getExecutionStatsLocked(0, "com.android.test", ACTIVE_INDEX));
+
+ // Working
+ expectedStats.windowSizeMs = 2 * HOUR_IN_MILLIS;
+ expectedStats.invalidTimeElapsed = now;
+ expectedStats.executionTimeInWindowMs = 13 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInWindow = 10;
+ expectedStats.executionTimeInMaxPeriodMs = 33 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInMaxPeriod = 20;
+ expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - 3 * MINUTE_IN_MILLIS)
+ + mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS;
+ assertEquals(expectedStats,
+ mQuotaController.getExecutionStatsLocked(0, "com.android.test", WORKING_INDEX));
+
+ // Frequent
+ expectedStats.windowSizeMs = 8 * HOUR_IN_MILLIS;
+ expectedStats.invalidTimeElapsed = now + HOUR_IN_MILLIS;
+ expectedStats.executionTimeInWindowMs = 23 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInWindow = 15;
+ expectedStats.executionTimeInMaxPeriodMs = 33 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInMaxPeriod = 20;
+ expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - 3 * MINUTE_IN_MILLIS)
+ + mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS;
+ assertEquals(expectedStats,
+ mQuotaController.getExecutionStatsLocked(0, "com.android.test", FREQUENT_INDEX));
+
+ // Rare
+ expectedStats.windowSizeMs = 24 * HOUR_IN_MILLIS;
+ expectedStats.invalidTimeElapsed = now + HOUR_IN_MILLIS;
+ expectedStats.executionTimeInWindowMs = 33 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInWindow = 20;
+ expectedStats.executionTimeInMaxPeriodMs = 33 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInMaxPeriod = 20;
+ expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - 3 * MINUTE_IN_MILLIS)
+ + mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS;
+ assertEquals(expectedStats,
+ mQuotaController.getExecutionStatsLocked(0, "com.android.test", RARE_INDEX));
+ }
+
+ /**
+ * Tests that getExecutionStatsLocked properly caches the stats and returns the cached object.
+ */
+ @Test
+ public void testGetExecutionStatsLocked_Caching() {
+ final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+ mQuotaController.saveTimingSession(0, "com.android.test",
+ createTimingSession(now - (23 * HOUR_IN_MILLIS), 10 * MINUTE_IN_MILLIS, 5));
+ mQuotaController.saveTimingSession(0, "com.android.test",
+ createTimingSession(now - (7 * HOUR_IN_MILLIS), 10 * MINUTE_IN_MILLIS, 5));
+ mQuotaController.saveTimingSession(0, "com.android.test",
+ createTimingSession(now - (2 * HOUR_IN_MILLIS), 10 * MINUTE_IN_MILLIS, 5));
+ mQuotaController.saveTimingSession(0, "com.android.test",
+ createTimingSession(now - (6 * MINUTE_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5));
+ final ExecutionStats originalStatsActive = mQuotaController.getExecutionStatsLocked(0,
+ "com.android.test", ACTIVE_INDEX);
+ final ExecutionStats originalStatsWorking = mQuotaController.getExecutionStatsLocked(0,
+ "com.android.test", WORKING_INDEX);
+ final ExecutionStats originalStatsFrequent = mQuotaController.getExecutionStatsLocked(0,
+ "com.android.test", FREQUENT_INDEX);
+ final ExecutionStats originalStatsRare = mQuotaController.getExecutionStatsLocked(0,
+ "com.android.test", RARE_INDEX);
+
+ // Advance clock so that the working stats shouldn't be the same.
+ advanceElapsedClock(MINUTE_IN_MILLIS);
+ // Change frequent bucket size so that the stats need to be recalculated.
+ mConstants.QUOTA_CONTROLLER_WINDOW_SIZE_FREQUENT_MS = 6 * HOUR_IN_MILLIS;
+ mQuotaController.onConstantsUpdatedLocked();
+
+ ExecutionStats expectedStats = new ExecutionStats();
+ expectedStats.windowSizeMs = originalStatsActive.windowSizeMs;
+ expectedStats.invalidTimeElapsed = originalStatsActive.invalidTimeElapsed;
+ expectedStats.executionTimeInWindowMs = originalStatsActive.executionTimeInWindowMs;
+ expectedStats.bgJobCountInWindow = originalStatsActive.bgJobCountInWindow;
+ expectedStats.executionTimeInMaxPeriodMs = originalStatsActive.executionTimeInMaxPeriodMs;
+ expectedStats.bgJobCountInMaxPeriod = originalStatsActive.bgJobCountInMaxPeriod;
+ expectedStats.quotaCutoffTimeElapsed = originalStatsActive.quotaCutoffTimeElapsed;
+ final ExecutionStats newStatsActive = mQuotaController.getExecutionStatsLocked(0,
+ "com.android.test", ACTIVE_INDEX);
+ // Stats for the same bucket should use the same object.
+ assertTrue(originalStatsActive == newStatsActive);
+ assertEquals(expectedStats, newStatsActive);
+
+ expectedStats.windowSizeMs = originalStatsWorking.windowSizeMs;
+ expectedStats.invalidTimeElapsed = originalStatsWorking.invalidTimeElapsed;
+ expectedStats.executionTimeInWindowMs = originalStatsWorking.executionTimeInWindowMs;
+ expectedStats.bgJobCountInWindow = originalStatsWorking.bgJobCountInWindow;
+ expectedStats.quotaCutoffTimeElapsed = originalStatsWorking.quotaCutoffTimeElapsed;
+ final ExecutionStats newStatsWorking = mQuotaController.getExecutionStatsLocked(0,
+ "com.android.test", WORKING_INDEX);
+ assertTrue(originalStatsWorking == newStatsWorking);
+ assertNotEquals(expectedStats, newStatsWorking);
+
+ expectedStats.windowSizeMs = originalStatsFrequent.windowSizeMs;
+ expectedStats.invalidTimeElapsed = originalStatsFrequent.invalidTimeElapsed;
+ expectedStats.executionTimeInWindowMs = originalStatsFrequent.executionTimeInWindowMs;
+ expectedStats.bgJobCountInWindow = originalStatsFrequent.bgJobCountInWindow;
+ expectedStats.quotaCutoffTimeElapsed = originalStatsFrequent.quotaCutoffTimeElapsed;
+ final ExecutionStats newStatsFrequent = mQuotaController.getExecutionStatsLocked(0,
+ "com.android.test", FREQUENT_INDEX);
+ assertTrue(originalStatsFrequent == newStatsFrequent);
+ assertNotEquals(expectedStats, newStatsFrequent);
+
+ expectedStats.windowSizeMs = originalStatsRare.windowSizeMs;
+ expectedStats.invalidTimeElapsed = originalStatsRare.invalidTimeElapsed;
+ expectedStats.executionTimeInWindowMs = originalStatsRare.executionTimeInWindowMs;
+ expectedStats.bgJobCountInWindow = originalStatsRare.bgJobCountInWindow;
+ expectedStats.quotaCutoffTimeElapsed = originalStatsRare.quotaCutoffTimeElapsed;
+ final ExecutionStats newStatsRare = mQuotaController.getExecutionStatsLocked(0,
+ "com.android.test", RARE_INDEX);
+ assertTrue(originalStatsRare == newStatsRare);
+ assertEquals(expectedStats, newStatsRare);
}
@Test
@@ -394,6 +673,56 @@
}
@Test
+ public void testMaybeScheduleStartAlarmLocked_Active() {
+ // saveTimingSession calls maybeScheduleCleanupAlarmLocked which interferes with these tests
+ // because it schedules an alarm too. Prevent it from doing so.
+ spyOn(mQuotaController);
+ doNothing().when(mQuotaController).maybeScheduleCleanupAlarmLocked();
+
+ // Active window size is 10 minutes.
+ final int standbyBucket = ACTIVE_INDEX;
+
+ // No sessions saved yet.
+ mQuotaController.maybeScheduleStartAlarmLocked(SOURCE_USER_ID, SOURCE_PACKAGE,
+ standbyBucket);
+ verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+
+ final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+ // Test with timing sessions out of window but still under max execution limit.
+ final long expectedAlarmTime =
+ (now - 18 * HOUR_IN_MILLIS) + 24 * HOUR_IN_MILLIS
+ + mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS;
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - 18 * HOUR_IN_MILLIS, HOUR_IN_MILLIS, 1));
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - 12 * HOUR_IN_MILLIS, HOUR_IN_MILLIS, 1));
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - 7 * HOUR_IN_MILLIS, HOUR_IN_MILLIS, 1));
+ mQuotaController.maybeScheduleStartAlarmLocked(SOURCE_USER_ID, SOURCE_PACKAGE,
+ standbyBucket);
+ verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - 2 * HOUR_IN_MILLIS, 55 * MINUTE_IN_MILLIS, 1));
+ mQuotaController.maybeScheduleStartAlarmLocked(SOURCE_USER_ID, SOURCE_PACKAGE,
+ standbyBucket);
+ verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+
+ JobStatus jobStatus = createJobStatus("testMaybeScheduleStartAlarmLocked_Active", 1);
+ mQuotaController.maybeStartTrackingJobLocked(jobStatus, null);
+ mQuotaController.prepareForExecutionLocked(jobStatus);
+ advanceElapsedClock(5 * MINUTE_IN_MILLIS);
+ // Timer has only been going for 5 minutes in the past 10 minutes, which is under the window
+ // size limit, but the total execution time for the past 24 hours is 6 hours, so the job no
+ // longer has quota.
+ assertEquals(0, mQuotaController.getRemainingExecutionTimeLocked(jobStatus));
+ mQuotaController.maybeScheduleStartAlarmLocked(SOURCE_USER_ID, SOURCE_PACKAGE,
+ standbyBucket);
+ verify(mAlarmManager, times(1)).set(anyInt(), eq(expectedAlarmTime), eq(TAG_QUOTA_CHECK),
+ any(), any());
+ }
+
+ @Test
public void testMaybeScheduleStartAlarmLocked_WorkingSet() {
// saveTimingSession calls maybeScheduleCleanupAlarmLocked which interferes with these tests
// because it schedules an alarm too. Prevent it from doing so.
@@ -579,8 +908,8 @@
// Start in ACTIVE bucket.
mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", ACTIVE_INDEX);
- inOrder.verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(),
- any());
+ inOrder.verify(mAlarmManager, never())
+ .set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
inOrder.verify(mAlarmManager, never()).cancel(any(AlarmManager.OnAlarmListener.class));
// And down from there.
@@ -620,6 +949,124 @@
inOrder.verify(mAlarmManager, times(1)).cancel(any(AlarmManager.OnAlarmListener.class));
}
+
+ /**
+ * Tests that the start alarm is properly rescheduled if the earliest session that contributes
+ * to the app being out of quota contributes less than the quota buffer time.
+ */
+ @Test
+ public void testMaybeScheduleStartAlarmLocked_SmallRollingQuota_DefaultValues() {
+ // Use the default values
+ runTestMaybeScheduleStartAlarmLocked_SmallRollingQuota_AllowedTimeCheck();
+ mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE).clear();
+ runTestMaybeScheduleStartAlarmLocked_SmallRollingQuota_MaxTimeCheck();
+ }
+
+ @Test
+ public void testMaybeScheduleStartAlarmLocked_SmallRollingQuota_UpdatedBufferSize() {
+ // Make sure any new value is used correctly.
+ mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS *= 2;
+ mQuotaController.onConstantsUpdatedLocked();
+ runTestMaybeScheduleStartAlarmLocked_SmallRollingQuota_AllowedTimeCheck();
+ mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE).clear();
+ runTestMaybeScheduleStartAlarmLocked_SmallRollingQuota_MaxTimeCheck();
+ }
+
+ @Test
+ public void testMaybeScheduleStartAlarmLocked_SmallRollingQuota_UpdatedAllowedTime() {
+ // Make sure any new value is used correctly.
+ mConstants.QUOTA_CONTROLLER_ALLOWED_TIME_PER_PERIOD_MS /= 2;
+ mQuotaController.onConstantsUpdatedLocked();
+ runTestMaybeScheduleStartAlarmLocked_SmallRollingQuota_AllowedTimeCheck();
+ mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE).clear();
+ runTestMaybeScheduleStartAlarmLocked_SmallRollingQuota_MaxTimeCheck();
+ }
+
+ @Test
+ public void testMaybeScheduleStartAlarmLocked_SmallRollingQuota_UpdatedMaxTime() {
+ // Make sure any new value is used correctly.
+ mConstants.QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS /= 2;
+ mQuotaController.onConstantsUpdatedLocked();
+ runTestMaybeScheduleStartAlarmLocked_SmallRollingQuota_AllowedTimeCheck();
+ mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE).clear();
+ runTestMaybeScheduleStartAlarmLocked_SmallRollingQuota_MaxTimeCheck();
+ }
+
+ @Test
+ public void testMaybeScheduleStartAlarmLocked_SmallRollingQuota_UpdatedEverything() {
+ // Make sure any new value is used correctly.
+ mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS *= 2;
+ mConstants.QUOTA_CONTROLLER_ALLOWED_TIME_PER_PERIOD_MS /= 2;
+ mConstants.QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS /= 2;
+ mQuotaController.onConstantsUpdatedLocked();
+ runTestMaybeScheduleStartAlarmLocked_SmallRollingQuota_AllowedTimeCheck();
+ mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE).clear();
+ runTestMaybeScheduleStartAlarmLocked_SmallRollingQuota_MaxTimeCheck();
+ }
+
+ private void runTestMaybeScheduleStartAlarmLocked_SmallRollingQuota_AllowedTimeCheck() {
+ // saveTimingSession calls maybeScheduleCleanupAlarmLocked which interferes with these tests
+ // because it schedules an alarm too. Prevent it from doing so.
+ spyOn(mQuotaController);
+ doNothing().when(mQuotaController).maybeScheduleCleanupAlarmLocked();
+
+ final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+ // Working set window size is 2 hours.
+ final int standbyBucket = WORKING_INDEX;
+ final long contributionMs = mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS / 2;
+ final long remainingTimeMs =
+ mConstants.QUOTA_CONTROLLER_ALLOWED_TIME_PER_PERIOD_MS - contributionMs;
+
+ // Session straddles edge of bucket window. Only the contribution should be counted towards
+ // the quota.
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - (2 * HOUR_IN_MILLIS + 3 * MINUTE_IN_MILLIS),
+ 3 * MINUTE_IN_MILLIS + contributionMs, 3));
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - HOUR_IN_MILLIS, remainingTimeMs, 2));
+ // Expected alarm time should be when the app will have QUOTA_BUFFER_MS time of quota, which
+ // is 2 hours + (QUOTA_BUFFER_MS - contributionMs) after the start of the second session.
+ final long expectedAlarmTime = now - HOUR_IN_MILLIS + 2 * HOUR_IN_MILLIS
+ + (mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS - contributionMs);
+ mQuotaController.maybeScheduleStartAlarmLocked(SOURCE_USER_ID, SOURCE_PACKAGE,
+ standbyBucket);
+ verify(mAlarmManager, times(1))
+ .set(anyInt(), eq(expectedAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
+ }
+
+
+ private void runTestMaybeScheduleStartAlarmLocked_SmallRollingQuota_MaxTimeCheck() {
+ // saveTimingSession calls maybeScheduleCleanupAlarmLocked which interferes with these tests
+ // because it schedules an alarm too. Prevent it from doing so.
+ spyOn(mQuotaController);
+ doNothing().when(mQuotaController).maybeScheduleCleanupAlarmLocked();
+
+ final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+ // Working set window size is 2 hours.
+ final int standbyBucket = WORKING_INDEX;
+ final long contributionMs = mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS / 2;
+ final long remainingTimeMs =
+ mConstants.QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS - contributionMs;
+
+ // Session straddles edge of 24 hour window. Only the contribution should be counted towards
+ // the quota.
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - (24 * HOUR_IN_MILLIS + 3 * MINUTE_IN_MILLIS),
+ 3 * MINUTE_IN_MILLIS + contributionMs, 3));
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - 20 * HOUR_IN_MILLIS, remainingTimeMs, 300));
+ // Expected alarm time should be when the app will have QUOTA_BUFFER_MS time of quota, which
+ // is 24 hours + (QUOTA_BUFFER_MS - contributionMs) after the start of the second session.
+ final long expectedAlarmTime = now - 20 * HOUR_IN_MILLIS
+ //+ mConstants.QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS
+ + 24 * HOUR_IN_MILLIS
+ + (mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS - contributionMs);
+ mQuotaController.maybeScheduleStartAlarmLocked(SOURCE_USER_ID, SOURCE_PACKAGE,
+ standbyBucket);
+ verify(mAlarmManager, times(1))
+ .set(anyInt(), eq(expectedAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
+ }
+
/** Tests that QuotaController doesn't throttle if throttling is turned off. */
@Test
public void testThrottleToggling() throws Exception {
@@ -652,6 +1099,7 @@
mConstants.QUOTA_CONTROLLER_WINDOW_SIZE_WORKING_MS = 30 * MINUTE_IN_MILLIS;
mConstants.QUOTA_CONTROLLER_WINDOW_SIZE_FREQUENT_MS = 45 * MINUTE_IN_MILLIS;
mConstants.QUOTA_CONTROLLER_WINDOW_SIZE_RARE_MS = 60 * MINUTE_IN_MILLIS;
+ mConstants.QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS = 3 * HOUR_IN_MILLIS;
mQuotaController.onConstantsUpdatedLocked();
@@ -662,6 +1110,7 @@
assertEquals(45 * MINUTE_IN_MILLIS,
mQuotaController.getBucketWindowSizes()[FREQUENT_INDEX]);
assertEquals(60 * MINUTE_IN_MILLIS, mQuotaController.getBucketWindowSizes()[RARE_INDEX]);
+ assertEquals(3 * HOUR_IN_MILLIS, mQuotaController.getMaxExecutionTimeMs());
}
@Test
@@ -673,6 +1122,7 @@
mConstants.QUOTA_CONTROLLER_WINDOW_SIZE_WORKING_MS = -MINUTE_IN_MILLIS;
mConstants.QUOTA_CONTROLLER_WINDOW_SIZE_FREQUENT_MS = -MINUTE_IN_MILLIS;
mConstants.QUOTA_CONTROLLER_WINDOW_SIZE_RARE_MS = -MINUTE_IN_MILLIS;
+ mConstants.QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS = -MINUTE_IN_MILLIS;
mQuotaController.onConstantsUpdatedLocked();
@@ -682,6 +1132,7 @@
assertEquals(MINUTE_IN_MILLIS, mQuotaController.getBucketWindowSizes()[WORKING_INDEX]);
assertEquals(MINUTE_IN_MILLIS, mQuotaController.getBucketWindowSizes()[FREQUENT_INDEX]);
assertEquals(MINUTE_IN_MILLIS, mQuotaController.getBucketWindowSizes()[RARE_INDEX]);
+ assertEquals(HOUR_IN_MILLIS, mQuotaController.getMaxExecutionTimeMs());
// Test larger than a day. Controller should cap at one day.
mConstants.QUOTA_CONTROLLER_ALLOWED_TIME_PER_PERIOD_MS = 25 * HOUR_IN_MILLIS;
@@ -690,6 +1141,7 @@
mConstants.QUOTA_CONTROLLER_WINDOW_SIZE_WORKING_MS = 25 * HOUR_IN_MILLIS;
mConstants.QUOTA_CONTROLLER_WINDOW_SIZE_FREQUENT_MS = 25 * HOUR_IN_MILLIS;
mConstants.QUOTA_CONTROLLER_WINDOW_SIZE_RARE_MS = 25 * HOUR_IN_MILLIS;
+ mConstants.QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS = 25 * HOUR_IN_MILLIS;
mQuotaController.onConstantsUpdatedLocked();
@@ -699,6 +1151,7 @@
assertEquals(24 * HOUR_IN_MILLIS, mQuotaController.getBucketWindowSizes()[WORKING_INDEX]);
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());
}
/** Tests that TimingSessions aren't saved when the device is charging. */
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/TimeControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/TimeControllerTest.java
new file mode 100644
index 0000000..494677d
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/TimeControllerTest.java
@@ -0,0 +1,886 @@
+/*
+ * Copyright (C) 2018 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.job.controllers;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.inOrder;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
+
+import static org.junit.Assert.assertFalse;
+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.anyString;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.app.AlarmManager;
+import android.app.job.JobInfo;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.PackageManagerInternal;
+import android.os.SystemClock;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.LocalServices;
+import com.android.server.job.JobSchedulerService;
+import com.android.server.job.JobSchedulerService.Constants;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.MockitoSession;
+import org.mockito.quality.Strictness;
+
+import java.time.Clock;
+import java.time.Duration;
+import java.time.ZoneOffset;
+
+@RunWith(AndroidJUnit4.class)
+public class TimeControllerTest {
+ private static final long SECOND_IN_MILLIS = 1000L;
+ private static final long MINUTE_IN_MILLIS = 60 * SECOND_IN_MILLIS;
+ private static final long HOUR_IN_MILLIS = 60 * MINUTE_IN_MILLIS;
+ private static final String TAG_DEADLINE = "*job.deadline*";
+ private static final String TAG_DELAY = "*job.delay*";
+ private static final String SOURCE_PACKAGE = "com.android.frameworks.mockingservicestests";
+ private static final int SOURCE_USER_ID = 0;
+
+ private Constants mConstants;
+ private TimeController mTimeController;
+
+ private MockitoSession mMockingSession;
+ @Mock
+ private AlarmManager mAlarmManager;
+ @Mock
+ private Context mContext;
+ @Mock
+ private JobSchedulerService mJobSchedulerService;
+
+ @Before
+ public void setUp() {
+ mMockingSession = mockitoSession()
+ .initMocks(this)
+ .strictness(Strictness.LENIENT)
+ .mockStatic(LocalServices.class)
+ .startMocking();
+ // Use default constants for now.
+ mConstants = new Constants();
+
+ // Called in StateController constructor.
+ when(mJobSchedulerService.getTestableContext()).thenReturn(mContext);
+ when(mJobSchedulerService.getLock()).thenReturn(mJobSchedulerService);
+ when(mJobSchedulerService.getConstants()).thenReturn(mConstants);
+ // Called in TimeController constructor.
+ when(mContext.getSystemService(Context.ALARM_SERVICE)).thenReturn(mAlarmManager);
+ // Used in JobStatus.
+ doReturn(mock(PackageManagerInternal.class))
+ .when(() -> LocalServices.getService(PackageManagerInternal.class));
+
+ // Freeze the clocks at this moment in time
+ JobSchedulerService.sSystemClock =
+ Clock.fixed(Clock.systemUTC().instant(), ZoneOffset.UTC);
+ JobSchedulerService.sUptimeMillisClock =
+ Clock.fixed(SystemClock.uptimeMillisClock().instant(), ZoneOffset.UTC);
+ JobSchedulerService.sElapsedRealtimeClock =
+ Clock.fixed(SystemClock.elapsedRealtimeClock().instant(), ZoneOffset.UTC);
+
+ // Initialize real objects.
+ mTimeController = new TimeController(mJobSchedulerService);
+ spyOn(mTimeController);
+ }
+
+ @After
+ public void tearDown() {
+ if (mMockingSession != null) {
+ mMockingSession.finishMocking();
+ }
+ }
+
+ private Clock getAdvancedClock(Clock clock, long incrementMs) {
+ return Clock.offset(clock, Duration.ofMillis(incrementMs));
+ }
+
+ private void advanceElapsedClock(long incrementMs) {
+ JobSchedulerService.sElapsedRealtimeClock = getAdvancedClock(
+ JobSchedulerService.sElapsedRealtimeClock, incrementMs);
+ }
+
+ private static JobInfo.Builder createJob() {
+ return new JobInfo.Builder(101, new ComponentName("foo", "bar"));
+ }
+
+ private JobStatus createJobStatus(String testTag, JobInfo.Builder job) {
+ JobInfo jobInfo = job.build();
+ return JobStatus.createFromJobInfo(
+ jobInfo, 1000, SOURCE_PACKAGE, SOURCE_USER_ID, testTag);
+ }
+
+ @Test
+ public void testMaybeStartTrackingJobLocked_AlreadySatisfied() {
+ JobStatus delaySatisfied = createJobStatus(
+ "testMaybeStartTrackingJobLocked_AlreadySatisfied",
+ createJob().setMinimumLatency(1));
+ JobStatus deadlineSatisfied = createJobStatus(
+ "testMaybeStartTrackingJobLocked_AlreadySatisfied",
+ createJob().setOverrideDeadline(1));
+
+ advanceElapsedClock(5);
+
+ mTimeController.maybeStartTrackingJobLocked(delaySatisfied, null);
+ mTimeController.maybeStartTrackingJobLocked(deadlineSatisfied, null);
+ verify(mAlarmManager, never())
+ .set(anyInt(), anyLong(), anyLong(), anyLong(), anyString(), any(), any(), any());
+ }
+
+ @Test
+ public void testMaybeStartTrackingJobLocked_DelayInOrder_NoSkipping() {
+ mConstants.TIME_CONTROLLER_SKIP_NOT_READY_JOBS = false;
+ mTimeController.onConstantsUpdatedLocked();
+
+ runTestMaybeStartTrackingJobLocked_DelayInOrder();
+ }
+
+ @Test
+ public void testMaybeStartTrackingJobLocked_DelayInOrder_WithSkipping_AllReady() {
+ mConstants.TIME_CONTROLLER_SKIP_NOT_READY_JOBS = true;
+ mTimeController.onConstantsUpdatedLocked();
+
+ doReturn(true).when(mTimeController).wouldBeReadyWithConstraintLocked(any(), anyInt());
+
+ runTestMaybeStartTrackingJobLocked_DelayInOrder();
+ }
+
+ private void runTestMaybeStartTrackingJobLocked_DelayInOrder() {
+ final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+
+ JobStatus jobLatest = createJobStatus("testMaybeStartTrackingJobLocked_DelayInOrder",
+ createJob().setMinimumLatency(HOUR_IN_MILLIS));
+ JobStatus jobMiddle = createJobStatus("testMaybeStartTrackingJobLocked_DelayInOrder",
+ createJob().setMinimumLatency(30 * MINUTE_IN_MILLIS));
+ JobStatus jobEarliest = createJobStatus("testMaybeStartTrackingJobLocked_DelayInOrder",
+ createJob().setMinimumLatency(5 * MINUTE_IN_MILLIS));
+
+ InOrder inOrder = inOrder(mAlarmManager);
+
+ mTimeController.maybeStartTrackingJobLocked(jobEarliest, null);
+ inOrder.verify(mAlarmManager, times(1))
+ .set(anyInt(), eq(now + 5 * MINUTE_IN_MILLIS), anyLong(), anyLong(), eq(TAG_DELAY),
+ any(), any(), any());
+ mTimeController.maybeStartTrackingJobLocked(jobMiddle, null);
+ inOrder.verify(mAlarmManager, never())
+ .set(anyInt(), anyLong(), anyLong(), anyLong(), anyString(), any(), any(), any());
+ mTimeController.maybeStartTrackingJobLocked(jobLatest, null);
+ inOrder.verify(mAlarmManager, never())
+ .set(anyInt(), anyLong(), anyLong(), anyLong(), anyString(), any(), any(), any());
+ }
+
+ @Test
+ public void testMaybeStartTrackingJobLocked_DelayInOrder_WithSkipping_SomeNotReady() {
+ mConstants.TIME_CONTROLLER_SKIP_NOT_READY_JOBS = true;
+ mTimeController.onConstantsUpdatedLocked();
+ final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+
+ JobStatus jobLatest = createJobStatus("testMaybeStartTrackingJobLocked_DelayInOrder",
+ createJob().setMinimumLatency(HOUR_IN_MILLIS));
+ JobStatus jobMiddle = createJobStatus("testMaybeStartTrackingJobLocked_DelayInOrder",
+ createJob().setMinimumLatency(30 * MINUTE_IN_MILLIS));
+ JobStatus jobEarliest = createJobStatus("testMaybeStartTrackingJobLocked_DelayInOrder",
+ createJob().setMinimumLatency(5 * MINUTE_IN_MILLIS));
+
+ doReturn(true).when(mTimeController)
+ .wouldBeReadyWithConstraintLocked(eq(jobLatest), anyInt());
+ doReturn(false).when(mTimeController)
+ .wouldBeReadyWithConstraintLocked(eq(jobMiddle), anyInt());
+ doReturn(true).when(mTimeController)
+ .wouldBeReadyWithConstraintLocked(eq(jobEarliest), anyInt());
+
+ InOrder inOrder = inOrder(mAlarmManager);
+
+ mTimeController.maybeStartTrackingJobLocked(jobEarliest, null);
+ inOrder.verify(mAlarmManager, times(1))
+ .set(anyInt(), eq(now + 5 * MINUTE_IN_MILLIS), anyLong(), anyLong(), eq(TAG_DELAY),
+ any(), any(), any());
+ mTimeController.maybeStartTrackingJobLocked(jobMiddle, null);
+ inOrder.verify(mAlarmManager, never())
+ .set(anyInt(), anyLong(), anyLong(), anyLong(), anyString(), any(), any(), any());
+ mTimeController.maybeStartTrackingJobLocked(jobLatest, null);
+ inOrder.verify(mAlarmManager, never())
+ .set(anyInt(), anyLong(), anyLong(), anyLong(), anyString(), any(), any(), any());
+ }
+
+ @Test
+ public void testMaybeStartTrackingJobLocked_DelayReverseOrder_NoSkipping() {
+ mConstants.TIME_CONTROLLER_SKIP_NOT_READY_JOBS = false;
+ mTimeController.onConstantsUpdatedLocked();
+
+ runTestMaybeStartTrackingJobLocked_DelayReverseOrder();
+ }
+
+ @Test
+ public void testMaybeStartTrackingJobLocked_DelayReverseOrder_WithSkipping_AllReady() {
+ mConstants.TIME_CONTROLLER_SKIP_NOT_READY_JOBS = true;
+ mTimeController.onConstantsUpdatedLocked();
+
+ doReturn(true).when(mTimeController).wouldBeReadyWithConstraintLocked(any(), anyInt());
+
+ runTestMaybeStartTrackingJobLocked_DelayReverseOrder();
+ }
+
+ private void runTestMaybeStartTrackingJobLocked_DelayReverseOrder() {
+ final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+
+ JobStatus jobLatest = createJobStatus("testMaybeStartTrackingJobLocked_DelayReverseOrder",
+ createJob().setMinimumLatency(HOUR_IN_MILLIS));
+ JobStatus jobMiddle = createJobStatus("testMaybeStartTrackingJobLocked_DelayReverseOrder",
+ createJob().setMinimumLatency(30 * MINUTE_IN_MILLIS));
+ JobStatus jobEarliest = createJobStatus("testMaybeStartTrackingJobLocked_DelayReverseOrder",
+ createJob().setMinimumLatency(5 * MINUTE_IN_MILLIS));
+
+ InOrder inOrder = inOrder(mAlarmManager);
+
+ mTimeController.maybeStartTrackingJobLocked(jobLatest, null);
+ inOrder.verify(mAlarmManager, times(1))
+ .set(anyInt(), eq(now + HOUR_IN_MILLIS), anyLong(), anyLong(), eq(TAG_DELAY), any(),
+ any(), any());
+ mTimeController.maybeStartTrackingJobLocked(jobMiddle, null);
+ inOrder.verify(mAlarmManager, times(1))
+ .set(anyInt(), eq(now + 30 * MINUTE_IN_MILLIS), anyLong(), anyLong(), eq(TAG_DELAY),
+ any(), any(), any());
+ mTimeController.maybeStartTrackingJobLocked(jobEarliest, null);
+ inOrder.verify(mAlarmManager, times(1))
+ .set(anyInt(), eq(now + 5 * MINUTE_IN_MILLIS), anyLong(), anyLong(), eq(TAG_DELAY),
+ any(), any(), any());
+ }
+
+ @Test
+ public void testMaybeStartTrackingJobLocked_DelayReverseOrder_WithSkipping_SomeNotReady() {
+ mConstants.TIME_CONTROLLER_SKIP_NOT_READY_JOBS = true;
+ mTimeController.onConstantsUpdatedLocked();
+ final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+
+ JobStatus jobLatest = createJobStatus("testMaybeStartTrackingJobLocked_DelayReverseOrder",
+ createJob().setMinimumLatency(HOUR_IN_MILLIS));
+ JobStatus jobMiddle = createJobStatus("testMaybeStartTrackingJobLocked_DelayReverseOrder",
+ createJob().setMinimumLatency(30 * MINUTE_IN_MILLIS));
+ JobStatus jobEarliest = createJobStatus("testMaybeStartTrackingJobLocked_DelayReverseOrder",
+ createJob().setMinimumLatency(5 * MINUTE_IN_MILLIS));
+
+ doReturn(true).when(mTimeController)
+ .wouldBeReadyWithConstraintLocked(eq(jobLatest), anyInt());
+ doReturn(false).when(mTimeController)
+ .wouldBeReadyWithConstraintLocked(eq(jobMiddle), anyInt());
+ doReturn(true).when(mTimeController)
+ .wouldBeReadyWithConstraintLocked(eq(jobEarliest), anyInt());
+
+ InOrder inOrder = inOrder(mAlarmManager);
+
+ mTimeController.maybeStartTrackingJobLocked(jobLatest, null);
+ inOrder.verify(mAlarmManager, times(1))
+ .set(anyInt(), eq(now + HOUR_IN_MILLIS), anyLong(), anyLong(), eq(TAG_DELAY), any(),
+ any(), any());
+ mTimeController.maybeStartTrackingJobLocked(jobMiddle, null);
+ // Middle alarm shouldn't be set since it won't be ready.
+ inOrder.verify(mAlarmManager, never())
+ .set(anyInt(), anyLong(), anyLong(), anyLong(), eq(TAG_DELAY), any(), any(), any());
+ mTimeController.maybeStartTrackingJobLocked(jobEarliest, null);
+ inOrder.verify(mAlarmManager, times(1))
+ .set(anyInt(), eq(now + 5 * MINUTE_IN_MILLIS), anyLong(), anyLong(), eq(TAG_DELAY),
+ any(), any(), any());
+ }
+
+ @Test
+ public void testMaybeStartTrackingJobLocked_DeadlineInOrder_NoSkipping() {
+ mConstants.TIME_CONTROLLER_SKIP_NOT_READY_JOBS = false;
+ mTimeController.onConstantsUpdatedLocked();
+
+ runTestMaybeStartTrackingJobLocked_DeadlineInOrder();
+ }
+
+ @Test
+ public void testMaybeStartTrackingJobLocked_DeadlineInOrder_WithSkipping_AllReady() {
+ mConstants.TIME_CONTROLLER_SKIP_NOT_READY_JOBS = true;
+ mTimeController.onConstantsUpdatedLocked();
+
+ doReturn(true).when(mTimeController).wouldBeReadyWithConstraintLocked(any(), anyInt());
+
+ runTestMaybeStartTrackingJobLocked_DeadlineInOrder();
+ }
+
+ private void runTestMaybeStartTrackingJobLocked_DeadlineInOrder() {
+ final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+
+ JobStatus jobLatest = createJobStatus("testMaybeStartTrackingJobLocked_DeadlineInOrder",
+ createJob().setOverrideDeadline(HOUR_IN_MILLIS));
+ JobStatus jobMiddle = createJobStatus("testMaybeStartTrackingJobLocked_DeadlineInOrder",
+ createJob().setOverrideDeadline(30 * MINUTE_IN_MILLIS));
+ JobStatus jobEarliest = createJobStatus("testMaybeStartTrackingJobLocked_DeadlineInOrder",
+ createJob().setOverrideDeadline(5 * MINUTE_IN_MILLIS));
+
+ InOrder inOrder = inOrder(mAlarmManager);
+
+ mTimeController.maybeStartTrackingJobLocked(jobEarliest, null);
+ inOrder.verify(mAlarmManager, times(1))
+ .set(anyInt(), eq(now + 5 * MINUTE_IN_MILLIS), anyLong(), anyLong(),
+ eq(TAG_DEADLINE), any(), any(), any());
+ mTimeController.maybeStartTrackingJobLocked(jobMiddle, null);
+ inOrder.verify(mAlarmManager, never())
+ .set(anyInt(), anyLong(), anyLong(), anyLong(), anyString(), any(), any(), any());
+ mTimeController.maybeStartTrackingJobLocked(jobLatest, null);
+ inOrder.verify(mAlarmManager, never())
+ .set(anyInt(), anyLong(), anyLong(), anyLong(), anyString(), any(), any(), any());
+ }
+
+ @Test
+ public void testMaybeStartTrackingJobLocked_DeadlineInOrder_WithSkipping_SomeNotReady() {
+ mConstants.TIME_CONTROLLER_SKIP_NOT_READY_JOBS = true;
+ mTimeController.onConstantsUpdatedLocked();
+ final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+
+ JobStatus jobLatest = createJobStatus("testMaybeStartTrackingJobLocked_DeadlineInOrder",
+ createJob().setOverrideDeadline(HOUR_IN_MILLIS));
+ JobStatus jobMiddle = createJobStatus("testMaybeStartTrackingJobLocked_DeadlineInOrder",
+ createJob().setOverrideDeadline(30 * MINUTE_IN_MILLIS));
+ JobStatus jobEarliest = createJobStatus("testMaybeStartTrackingJobLocked_DeadlineInOrder",
+ createJob().setOverrideDeadline(5 * MINUTE_IN_MILLIS));
+
+ doReturn(true).when(mTimeController)
+ .wouldBeReadyWithConstraintLocked(eq(jobLatest), anyInt());
+ doReturn(false).when(mTimeController)
+ .wouldBeReadyWithConstraintLocked(eq(jobMiddle), anyInt());
+ doReturn(true).when(mTimeController)
+ .wouldBeReadyWithConstraintLocked(eq(jobEarliest), anyInt());
+
+ InOrder inOrder = inOrder(mAlarmManager);
+
+ mTimeController.maybeStartTrackingJobLocked(jobEarliest, null);
+ inOrder.verify(mAlarmManager, times(1))
+ .set(anyInt(), eq(now + 5 * MINUTE_IN_MILLIS), anyLong(), anyLong(),
+ eq(TAG_DEADLINE), any(), any(), any());
+ mTimeController.maybeStartTrackingJobLocked(jobMiddle, null);
+ inOrder.verify(mAlarmManager, never())
+ .set(anyInt(), anyLong(), anyLong(), anyLong(), anyString(), any(), any(), any());
+ mTimeController.maybeStartTrackingJobLocked(jobLatest, null);
+ inOrder.verify(mAlarmManager, never())
+ .set(anyInt(), anyLong(), anyLong(), anyLong(), anyString(), any(), any(), any());
+ }
+
+ @Test
+ public void testMaybeStartTrackingJobLocked_DeadlineReverseOrder_NoSkipping() {
+ mConstants.TIME_CONTROLLER_SKIP_NOT_READY_JOBS = false;
+ mTimeController.onConstantsUpdatedLocked();
+
+ runTestMaybeStartTrackingJobLocked_DeadlineReverseOrder();
+ }
+
+ @Test
+ public void testMaybeStartTrackingJobLocked_DeadlineReverseOrder_WithSkipping_AllReady() {
+ mConstants.TIME_CONTROLLER_SKIP_NOT_READY_JOBS = true;
+ mTimeController.onConstantsUpdatedLocked();
+
+ doReturn(true).when(mTimeController).wouldBeReadyWithConstraintLocked(any(), anyInt());
+
+ runTestMaybeStartTrackingJobLocked_DeadlineReverseOrder();
+ }
+
+ private void runTestMaybeStartTrackingJobLocked_DeadlineReverseOrder() {
+ final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+
+ JobStatus jobLatest = createJobStatus(
+ "testMaybeStartTrackingJobLocked_DeadlineReverseOrder",
+ createJob().setOverrideDeadline(HOUR_IN_MILLIS));
+ JobStatus jobMiddle = createJobStatus(
+ "testMaybeStartTrackingJobLocked_DeadlineReverseOrder",
+ createJob().setOverrideDeadline(30 * MINUTE_IN_MILLIS));
+ JobStatus jobEarliest = createJobStatus(
+ "testMaybeStartTrackingJobLocked_DeadlineReverseOrder",
+ createJob().setOverrideDeadline(5 * MINUTE_IN_MILLIS));
+
+ InOrder inOrder = inOrder(mAlarmManager);
+
+ mTimeController.maybeStartTrackingJobLocked(jobLatest, null);
+ inOrder.verify(mAlarmManager, times(1))
+ .set(anyInt(), eq(now + HOUR_IN_MILLIS), anyLong(), anyLong(), eq(TAG_DEADLINE),
+ any(), any(), any());
+ mTimeController.maybeStartTrackingJobLocked(jobMiddle, null);
+ inOrder.verify(mAlarmManager, times(1))
+ .set(anyInt(), eq(now + 30 * MINUTE_IN_MILLIS), anyLong(), anyLong(),
+ eq(TAG_DEADLINE), any(), any(), any());
+ mTimeController.maybeStartTrackingJobLocked(jobEarliest, null);
+ inOrder.verify(mAlarmManager, times(1))
+ .set(anyInt(), eq(now + 5 * MINUTE_IN_MILLIS), anyLong(), anyLong(),
+ eq(TAG_DEADLINE), any(), any(), any());
+ }
+
+ @Test
+ public void testMaybeStartTrackingJobLocked_DeadlineReverseOrder_WithSkipping_SomeNotReady() {
+ mConstants.TIME_CONTROLLER_SKIP_NOT_READY_JOBS = true;
+ mTimeController.onConstantsUpdatedLocked();
+ final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+
+ JobStatus jobLatest = createJobStatus(
+ "testMaybeStartTrackingJobLocked_DeadlineReverseOrder",
+ createJob().setOverrideDeadline(HOUR_IN_MILLIS));
+ JobStatus jobMiddle = createJobStatus(
+ "testMaybeStartTrackingJobLocked_DeadlineReverseOrder",
+ createJob().setOverrideDeadline(30 * MINUTE_IN_MILLIS));
+ JobStatus jobEarliest = createJobStatus(
+ "testMaybeStartTrackingJobLocked_DeadlineReverseOrder",
+ createJob().setOverrideDeadline(5 * MINUTE_IN_MILLIS));
+
+ doReturn(true).when(mTimeController)
+ .wouldBeReadyWithConstraintLocked(eq(jobLatest), anyInt());
+ doReturn(false).when(mTimeController)
+ .wouldBeReadyWithConstraintLocked(eq(jobMiddle), anyInt());
+ doReturn(true).when(mTimeController)
+ .wouldBeReadyWithConstraintLocked(eq(jobEarliest), anyInt());
+
+ InOrder inOrder = inOrder(mAlarmManager);
+
+ mTimeController.maybeStartTrackingJobLocked(jobLatest, null);
+ inOrder.verify(mAlarmManager, times(1))
+ .set(anyInt(), eq(now + HOUR_IN_MILLIS), anyLong(), anyLong(), eq(TAG_DEADLINE),
+ any(), any(), any());
+ mTimeController.maybeStartTrackingJobLocked(jobMiddle, null);
+ // Middle alarm should be skipped since the job wouldn't be ready.
+ inOrder.verify(mAlarmManager, never())
+ .set(anyInt(), anyLong(), anyLong(), anyLong(), eq(TAG_DEADLINE), any(), any(),
+ any());
+ mTimeController.maybeStartTrackingJobLocked(jobEarliest, null);
+ inOrder.verify(mAlarmManager, times(1))
+ .set(anyInt(), eq(now + 5 * MINUTE_IN_MILLIS), anyLong(), anyLong(),
+ eq(TAG_DEADLINE), any(), any(), any());
+ }
+
+ @Test
+ public void testJobSkipToggling() {
+ final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+
+ JobStatus jobLatest = createJobStatus(
+ "testMaybeStartTrackingJobLocked_DeadlineReverseOrder",
+ createJob().setOverrideDeadline(HOUR_IN_MILLIS));
+ JobStatus jobEarliest = createJobStatus(
+ "testMaybeStartTrackingJobLocked_DeadlineReverseOrder",
+ createJob().setOverrideDeadline(5 * MINUTE_IN_MILLIS));
+
+ doReturn(true).when(mTimeController)
+ .wouldBeReadyWithConstraintLocked(eq(jobLatest), anyInt());
+ doReturn(false).when(mTimeController)
+ .wouldBeReadyWithConstraintLocked(eq(jobEarliest), anyInt());
+
+ // Starting off with the skipping off, we should still set an alarm for the earlier job.
+ mConstants.TIME_CONTROLLER_SKIP_NOT_READY_JOBS = false;
+ mTimeController.onConstantsUpdatedLocked();
+ InOrder inOrder = inOrder(mAlarmManager);
+
+ mTimeController.maybeStartTrackingJobLocked(jobEarliest, null);
+ mTimeController.maybeStartTrackingJobLocked(jobLatest, null);
+ inOrder.verify(mAlarmManager, times(1))
+ .set(anyInt(), eq(now + 5 * MINUTE_IN_MILLIS), anyLong(), anyLong(),
+ eq(TAG_DEADLINE), any(), any(), any());
+
+ // Turn it on, use alarm for later job.
+ mConstants.TIME_CONTROLLER_SKIP_NOT_READY_JOBS = true;
+ mTimeController.onConstantsUpdatedLocked();
+
+ inOrder.verify(mAlarmManager, times(1))
+ .set(anyInt(), eq(now + HOUR_IN_MILLIS), anyLong(), anyLong(), eq(TAG_DEADLINE),
+ any(), any(), any());
+
+ // Back off, use alarm for earlier job.
+ mConstants.TIME_CONTROLLER_SKIP_NOT_READY_JOBS = false;
+ mTimeController.onConstantsUpdatedLocked();
+
+ inOrder.verify(mAlarmManager, times(1))
+ .set(anyInt(), eq(now + 5 * MINUTE_IN_MILLIS), anyLong(), anyLong(),
+ eq(TAG_DEADLINE), any(), any(), any());
+ }
+
+ @Test
+ public void testCheckExpiredDelaysAndResetAlarm_NoSkipping() {
+ mConstants.TIME_CONTROLLER_SKIP_NOT_READY_JOBS = false;
+ mTimeController.onConstantsUpdatedLocked();
+
+ runTestCheckExpiredDelaysAndResetAlarm();
+ }
+
+ @Test
+ public void testCheckExpiredDelaysAndResetAlarm_WithSkipping_AllReady() {
+ mConstants.TIME_CONTROLLER_SKIP_NOT_READY_JOBS = true;
+ mTimeController.onConstantsUpdatedLocked();
+
+ doReturn(true).when(mTimeController).wouldBeReadyWithConstraintLocked(any(), anyInt());
+
+ runTestCheckExpiredDelaysAndResetAlarm();
+ }
+
+ private void runTestCheckExpiredDelaysAndResetAlarm() {
+ final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+
+ JobStatus jobLatest = createJobStatus("testCheckExpiredDelaysAndResetAlarm",
+ createJob().setMinimumLatency(HOUR_IN_MILLIS));
+ JobStatus jobMiddle = createJobStatus("testCheckExpiredDelaysAndResetAlarm",
+ createJob().setMinimumLatency(30 * MINUTE_IN_MILLIS));
+ JobStatus jobEarliest = createJobStatus("testCheckExpiredDelaysAndResetAlarm",
+ createJob().setMinimumLatency(5 * MINUTE_IN_MILLIS));
+
+ InOrder inOrder = inOrder(mAlarmManager);
+
+ mTimeController.maybeStartTrackingJobLocked(jobLatest, null);
+ mTimeController.maybeStartTrackingJobLocked(jobMiddle, null);
+ mTimeController.maybeStartTrackingJobLocked(jobEarliest, null);
+ inOrder.verify(mAlarmManager, times(1))
+ .set(anyInt(), eq(now + 5 * MINUTE_IN_MILLIS), anyLong(), anyLong(), eq(TAG_DELAY),
+ any(), any(), any());
+
+ advanceElapsedClock(10 * MINUTE_IN_MILLIS);
+
+ mTimeController.checkExpiredDelaysAndResetAlarm();
+ assertTrue(jobEarliest.isConstraintSatisfied(JobStatus.CONSTRAINT_TIMING_DELAY));
+ assertFalse(jobMiddle.isConstraintSatisfied(JobStatus.CONSTRAINT_TIMING_DELAY));
+ assertFalse(jobLatest.isConstraintSatisfied(JobStatus.CONSTRAINT_TIMING_DELAY));
+ inOrder.verify(mAlarmManager, times(1))
+ .set(anyInt(), eq(now + 30 * MINUTE_IN_MILLIS), anyLong(), anyLong(), eq(TAG_DELAY),
+ any(), any(), any());
+
+ advanceElapsedClock(30 * MINUTE_IN_MILLIS);
+
+ mTimeController.checkExpiredDelaysAndResetAlarm();
+ assertTrue(jobEarliest.isConstraintSatisfied(JobStatus.CONSTRAINT_TIMING_DELAY));
+ assertTrue(jobMiddle.isConstraintSatisfied(JobStatus.CONSTRAINT_TIMING_DELAY));
+ assertFalse(jobLatest.isConstraintSatisfied(JobStatus.CONSTRAINT_TIMING_DELAY));
+ inOrder.verify(mAlarmManager, times(1))
+ .set(anyInt(), eq(now + HOUR_IN_MILLIS), anyLong(), anyLong(), eq(TAG_DELAY), any(),
+ any(), any());
+
+ advanceElapsedClock(30 * MINUTE_IN_MILLIS);
+
+ mTimeController.checkExpiredDelaysAndResetAlarm();
+ assertTrue(jobEarliest.isConstraintSatisfied(JobStatus.CONSTRAINT_TIMING_DELAY));
+ assertTrue(jobMiddle.isConstraintSatisfied(JobStatus.CONSTRAINT_TIMING_DELAY));
+ assertTrue(jobLatest.isConstraintSatisfied(JobStatus.CONSTRAINT_TIMING_DELAY));
+ inOrder.verify(mAlarmManager, never())
+ .set(anyInt(), anyLong(), anyLong(), anyLong(), anyString(), any(), any(), any());
+ }
+
+ @Test
+ public void testCheckExpiredDelaysAndResetAlarm_WithSkipping_SomeNotReady() {
+ mConstants.TIME_CONTROLLER_SKIP_NOT_READY_JOBS = true;
+ mTimeController.onConstantsUpdatedLocked();
+ final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+
+ JobStatus jobLatest = createJobStatus("testCheckExpiredDelaysAndResetAlarm",
+ createJob().setMinimumLatency(HOUR_IN_MILLIS));
+ JobStatus jobMiddle = createJobStatus("testCheckExpiredDelaysAndResetAlarm",
+ createJob().setMinimumLatency(30 * MINUTE_IN_MILLIS));
+ JobStatus jobEarliest = createJobStatus("testCheckExpiredDelaysAndResetAlarm",
+ createJob().setMinimumLatency(5 * MINUTE_IN_MILLIS));
+
+ doReturn(true).when(mTimeController)
+ .wouldBeReadyWithConstraintLocked(eq(jobLatest), anyInt());
+ doReturn(false).when(mTimeController)
+ .wouldBeReadyWithConstraintLocked(eq(jobMiddle), anyInt());
+ doReturn(true).when(mTimeController)
+ .wouldBeReadyWithConstraintLocked(eq(jobEarliest), anyInt());
+
+ InOrder inOrder = inOrder(mAlarmManager);
+
+ mTimeController.maybeStartTrackingJobLocked(jobLatest, null);
+ mTimeController.maybeStartTrackingJobLocked(jobMiddle, null);
+ mTimeController.maybeStartTrackingJobLocked(jobEarliest, null);
+ inOrder.verify(mAlarmManager, times(1))
+ .set(anyInt(), eq(now + 5 * MINUTE_IN_MILLIS), anyLong(), anyLong(), eq(TAG_DELAY),
+ any(), any(), any());
+
+ advanceElapsedClock(10 * MINUTE_IN_MILLIS);
+
+ mTimeController.checkExpiredDelaysAndResetAlarm();
+ assertTrue(jobEarliest.isConstraintSatisfied(JobStatus.CONSTRAINT_TIMING_DELAY));
+ assertFalse(jobMiddle.isConstraintSatisfied(JobStatus.CONSTRAINT_TIMING_DELAY));
+ assertFalse(jobLatest.isConstraintSatisfied(JobStatus.CONSTRAINT_TIMING_DELAY));
+ // Middle job wouldn't be ready, so its alarm should be skipped.
+ inOrder.verify(mAlarmManager, times(1))
+ .set(anyInt(), eq(now + HOUR_IN_MILLIS), anyLong(), anyLong(), eq(TAG_DELAY), any(),
+ any(), any());
+
+ advanceElapsedClock(55 * MINUTE_IN_MILLIS);
+
+ mTimeController.checkExpiredDelaysAndResetAlarm();
+ assertTrue(jobEarliest.isConstraintSatisfied(JobStatus.CONSTRAINT_TIMING_DELAY));
+ assertTrue(jobMiddle.isConstraintSatisfied(JobStatus.CONSTRAINT_TIMING_DELAY));
+ assertTrue(jobLatest.isConstraintSatisfied(JobStatus.CONSTRAINT_TIMING_DELAY));
+ inOrder.verify(mAlarmManager, never())
+ .set(anyInt(), anyLong(), anyLong(), anyLong(), anyString(), any(), any(), any());
+ }
+
+ @Test
+ public void testCheckExpiredDeadlinesAndResetAlarm_NoSkipping() {
+ mConstants.TIME_CONTROLLER_SKIP_NOT_READY_JOBS = false;
+ mTimeController.onConstantsUpdatedLocked();
+
+ runTestCheckExpiredDeadlinesAndResetAlarm();
+ }
+
+ @Test
+ public void testCheckExpiredDeadlinesAndResetAlarm_WithSkipping_AllReady() {
+ mConstants.TIME_CONTROLLER_SKIP_NOT_READY_JOBS = true;
+ mTimeController.onConstantsUpdatedLocked();
+
+ doReturn(true).when(mTimeController).wouldBeReadyWithConstraintLocked(any(), anyInt());
+
+ runTestCheckExpiredDeadlinesAndResetAlarm();
+ }
+
+ private void runTestCheckExpiredDeadlinesAndResetAlarm() {
+ final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+
+ JobStatus jobLatest = createJobStatus("testCheckExpiredDeadlinesAndResetAlarm",
+ createJob().setOverrideDeadline(HOUR_IN_MILLIS));
+ JobStatus jobMiddle = createJobStatus("testCheckExpiredDeadlinesAndResetAlarm",
+ createJob().setOverrideDeadline(30 * MINUTE_IN_MILLIS));
+ JobStatus jobEarliest = createJobStatus("testCheckExpiredDeadlinesAndResetAlarm",
+ createJob().setOverrideDeadline(5 * MINUTE_IN_MILLIS));
+
+ InOrder inOrder = inOrder(mAlarmManager);
+
+ mTimeController.maybeStartTrackingJobLocked(jobLatest, null);
+ mTimeController.maybeStartTrackingJobLocked(jobMiddle, null);
+ mTimeController.maybeStartTrackingJobLocked(jobEarliest, null);
+ inOrder.verify(mAlarmManager, times(1))
+ .set(anyInt(), eq(now + 5 * MINUTE_IN_MILLIS), anyLong(), anyLong(),
+ eq(TAG_DEADLINE), any(), any(), any());
+
+ advanceElapsedClock(10 * MINUTE_IN_MILLIS);
+
+ mTimeController.checkExpiredDeadlinesAndResetAlarm();
+ assertTrue(jobEarliest.isConstraintSatisfied(JobStatus.CONSTRAINT_DEADLINE));
+ assertFalse(jobMiddle.isConstraintSatisfied(JobStatus.CONSTRAINT_DEADLINE));
+ assertFalse(jobLatest.isConstraintSatisfied(JobStatus.CONSTRAINT_DEADLINE));
+ inOrder.verify(mAlarmManager, times(1))
+ .set(anyInt(), eq(now + 30 * MINUTE_IN_MILLIS), anyLong(), anyLong(),
+ eq(TAG_DEADLINE), any(), any(), any());
+
+ advanceElapsedClock(30 * MINUTE_IN_MILLIS);
+
+ mTimeController.checkExpiredDeadlinesAndResetAlarm();
+ assertTrue(jobEarliest.isConstraintSatisfied(JobStatus.CONSTRAINT_DEADLINE));
+ assertTrue(jobMiddle.isConstraintSatisfied(JobStatus.CONSTRAINT_DEADLINE));
+ assertFalse(jobLatest.isConstraintSatisfied(JobStatus.CONSTRAINT_DEADLINE));
+ inOrder.verify(mAlarmManager, times(1))
+ .set(anyInt(), eq(now + HOUR_IN_MILLIS), anyLong(), anyLong(), eq(TAG_DEADLINE),
+ any(), any(), any());
+
+ advanceElapsedClock(30 * MINUTE_IN_MILLIS);
+
+ mTimeController.checkExpiredDeadlinesAndResetAlarm();
+ assertTrue(jobEarliest.isConstraintSatisfied(JobStatus.CONSTRAINT_DEADLINE));
+ assertTrue(jobMiddle.isConstraintSatisfied(JobStatus.CONSTRAINT_DEADLINE));
+ assertTrue(jobLatest.isConstraintSatisfied(JobStatus.CONSTRAINT_DEADLINE));
+ inOrder.verify(mAlarmManager, never())
+ .set(anyInt(), anyLong(), anyLong(), anyLong(), anyString(), any(), any(), any());
+ }
+
+ @Test
+ public void testCheckExpiredDeadlinesAndResetAlarm_WithSkipping_SomeNotReady() {
+ mConstants.TIME_CONTROLLER_SKIP_NOT_READY_JOBS = true;
+ mTimeController.onConstantsUpdatedLocked();
+ final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+
+ JobStatus jobLatest = createJobStatus("testCheckExpiredDeadlinesAndResetAlarm",
+ createJob().setOverrideDeadline(HOUR_IN_MILLIS));
+ JobStatus jobMiddle = createJobStatus("testCheckExpiredDeadlinesAndResetAlarm",
+ createJob().setOverrideDeadline(30 * MINUTE_IN_MILLIS));
+ JobStatus jobEarliest = createJobStatus("testCheckExpiredDeadlinesAndResetAlarm",
+ createJob().setOverrideDeadline(5 * MINUTE_IN_MILLIS));
+
+ doReturn(true).when(mTimeController)
+ .wouldBeReadyWithConstraintLocked(eq(jobLatest), anyInt());
+ doReturn(false).when(mTimeController)
+ .wouldBeReadyWithConstraintLocked(eq(jobMiddle), anyInt());
+ doReturn(true).when(mTimeController)
+ .wouldBeReadyWithConstraintLocked(eq(jobEarliest), anyInt());
+
+ InOrder inOrder = inOrder(mAlarmManager);
+
+ mTimeController.maybeStartTrackingJobLocked(jobLatest, null);
+ mTimeController.maybeStartTrackingJobLocked(jobMiddle, null);
+ mTimeController.maybeStartTrackingJobLocked(jobEarliest, null);
+ inOrder.verify(mAlarmManager, times(1))
+ .set(anyInt(), eq(now + 5 * MINUTE_IN_MILLIS), anyLong(), anyLong(),
+ eq(TAG_DEADLINE), any(), any(), any());
+
+ advanceElapsedClock(10 * MINUTE_IN_MILLIS);
+
+ mTimeController.checkExpiredDeadlinesAndResetAlarm();
+ assertTrue(jobEarliest.isConstraintSatisfied(JobStatus.CONSTRAINT_DEADLINE));
+ assertFalse(jobMiddle.isConstraintSatisfied(JobStatus.CONSTRAINT_DEADLINE));
+ assertFalse(jobLatest.isConstraintSatisfied(JobStatus.CONSTRAINT_DEADLINE));
+ // Middle job wouldn't be ready, so its alarm should be skipped.
+ inOrder.verify(mAlarmManager, times(1))
+ .set(anyInt(), eq(now + HOUR_IN_MILLIS), anyLong(), anyLong(), eq(TAG_DEADLINE),
+ any(), any(), any());
+
+ advanceElapsedClock(55 * MINUTE_IN_MILLIS);
+
+ mTimeController.checkExpiredDeadlinesAndResetAlarm();
+ assertTrue(jobEarliest.isConstraintSatisfied(JobStatus.CONSTRAINT_DEADLINE));
+ assertTrue(jobMiddle.isConstraintSatisfied(JobStatus.CONSTRAINT_DEADLINE));
+ assertTrue(jobLatest.isConstraintSatisfied(JobStatus.CONSTRAINT_DEADLINE));
+ inOrder.verify(mAlarmManager, never())
+ .set(anyInt(), anyLong(), anyLong(), anyLong(), anyString(), any(), any(), any());
+ }
+
+ @Test
+ public void testEvaluateStateLocked_SkippingOff() {
+ mConstants.TIME_CONTROLLER_SKIP_NOT_READY_JOBS = false;
+ mTimeController.onConstantsUpdatedLocked();
+ JobStatus job = createJobStatus("testEvaluateStateLocked_SkippingOff",
+ createJob().setOverrideDeadline(HOUR_IN_MILLIS));
+
+ mTimeController.evaluateStateLocked(job);
+ verify(mAlarmManager, never())
+ .set(anyInt(), anyLong(), anyLong(), anyLong(), anyString(), any(), any(), any());
+ }
+
+ @Test
+ public void testEvaluateStateLocked_SkippingOn_Delay() {
+ mConstants.TIME_CONTROLLER_SKIP_NOT_READY_JOBS = true;
+ mTimeController.onConstantsUpdatedLocked();
+ final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+
+ JobStatus jobLatest = createJobStatus("testEvaluateStateLocked_SkippingOn_Delay",
+ createJob().setMinimumLatency(HOUR_IN_MILLIS));
+ JobStatus jobMiddle = createJobStatus("testEvaluateStateLocked_SkippingOn_Delay",
+ createJob().setMinimumLatency(30 * MINUTE_IN_MILLIS));
+ JobStatus jobEarliest = createJobStatus("testEvaluateStateLocked_SkippingOn_Delay",
+ createJob().setMinimumLatency(5 * MINUTE_IN_MILLIS));
+
+ doReturn(false).when(mTimeController)
+ .wouldBeReadyWithConstraintLocked(eq(jobLatest), anyInt());
+ doReturn(true).when(mTimeController)
+ .wouldBeReadyWithConstraintLocked(eq(jobMiddle), anyInt());
+ doReturn(false).when(mTimeController)
+ .wouldBeReadyWithConstraintLocked(eq(jobEarliest), anyInt());
+
+ InOrder inOrder = inOrder(mAlarmManager);
+
+ mTimeController.maybeStartTrackingJobLocked(jobLatest, null);
+ mTimeController.maybeStartTrackingJobLocked(jobMiddle, null);
+ mTimeController.maybeStartTrackingJobLocked(jobEarliest, null);
+ inOrder.verify(mAlarmManager, times(1))
+ .set(anyInt(), eq(now + 30 * MINUTE_IN_MILLIS), anyLong(), anyLong(), eq(TAG_DELAY),
+ any(), any(), any());
+
+ // Test evaluating something after the current deadline.
+ doReturn(true).when(mTimeController)
+ .wouldBeReadyWithConstraintLocked(eq(jobLatest), anyInt());
+ mTimeController.evaluateStateLocked(jobLatest);
+ inOrder.verify(mAlarmManager, never())
+ .set(anyInt(), anyLong(), anyLong(), anyLong(), anyString(), any(), any(), any());
+
+ // Test evaluating something before the current deadline.
+ doReturn(true).when(mTimeController)
+ .wouldBeReadyWithConstraintLocked(eq(jobEarliest), anyInt());
+ mTimeController.evaluateStateLocked(jobEarliest);
+ inOrder.verify(mAlarmManager, times(1))
+ .set(anyInt(), eq(now + 5 * MINUTE_IN_MILLIS), anyLong(), anyLong(), eq(TAG_DELAY),
+ any(), any(), any());
+ // Job goes back to not being ready. Middle is still true, so use that alarm.
+ doReturn(false).when(mTimeController)
+ .wouldBeReadyWithConstraintLocked(eq(jobEarliest), anyInt());
+ mTimeController.evaluateStateLocked(jobEarliest);
+ inOrder.verify(mAlarmManager, times(1))
+ .set(anyInt(), eq(now + 30 * MINUTE_IN_MILLIS), anyLong(), anyLong(),
+ eq(TAG_DELAY), any(), any(), any());
+ // Turn middle off. Latest is true, so use that alarm.
+ doReturn(false).when(mTimeController)
+ .wouldBeReadyWithConstraintLocked(eq(jobMiddle), anyInt());
+ mTimeController.evaluateStateLocked(jobMiddle);
+ inOrder.verify(mAlarmManager, times(1))
+ .set(anyInt(), eq(now + HOUR_IN_MILLIS), anyLong(), anyLong(),
+ eq(TAG_DELAY), any(), any(), any());
+ }
+
+ @Test
+ public void testEvaluateStateLocked_SkippingOn_Deadline() {
+ mConstants.TIME_CONTROLLER_SKIP_NOT_READY_JOBS = true;
+ mTimeController.onConstantsUpdatedLocked();
+ final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+
+ JobStatus jobLatest = createJobStatus("testEvaluateStateLocked_SkippingOn_Deadline",
+ createJob().setOverrideDeadline(HOUR_IN_MILLIS));
+ JobStatus jobMiddle = createJobStatus("testEvaluateStateLocked_SkippingOn_Deadline",
+ createJob().setOverrideDeadline(30 * MINUTE_IN_MILLIS));
+ JobStatus jobEarliest = createJobStatus("testEvaluateStateLocked_SkippingOn_Deadline",
+ createJob().setOverrideDeadline(5 * MINUTE_IN_MILLIS));
+
+ doReturn(false).when(mTimeController)
+ .wouldBeReadyWithConstraintLocked(eq(jobLatest), anyInt());
+ doReturn(true).when(mTimeController)
+ .wouldBeReadyWithConstraintLocked(eq(jobMiddle), anyInt());
+ doReturn(false).when(mTimeController)
+ .wouldBeReadyWithConstraintLocked(eq(jobEarliest), anyInt());
+
+ InOrder inOrder = inOrder(mAlarmManager);
+
+ mTimeController.maybeStartTrackingJobLocked(jobLatest, null);
+ mTimeController.maybeStartTrackingJobLocked(jobMiddle, null);
+ mTimeController.maybeStartTrackingJobLocked(jobEarliest, null);
+ inOrder.verify(mAlarmManager, times(1))
+ .set(anyInt(), eq(now + 30 * MINUTE_IN_MILLIS), anyLong(), anyLong(),
+ eq(TAG_DEADLINE), any(), any(), any());
+
+ // Test evaluating something after the current deadline.
+ doReturn(true).when(mTimeController)
+ .wouldBeReadyWithConstraintLocked(eq(jobLatest), anyInt());
+ mTimeController.evaluateStateLocked(jobLatest);
+ inOrder.verify(mAlarmManager, never())
+ .set(anyInt(), anyLong(), anyLong(), anyLong(), anyString(), any(), any(), any());
+
+ // Test evaluating something before the current deadline.
+ doReturn(true).when(mTimeController)
+ .wouldBeReadyWithConstraintLocked(eq(jobEarliest), anyInt());
+ mTimeController.evaluateStateLocked(jobEarliest);
+ inOrder.verify(mAlarmManager, times(1))
+ .set(anyInt(), eq(now + 5 * MINUTE_IN_MILLIS), anyLong(), anyLong(),
+ eq(TAG_DEADLINE), any(), any(), any());
+ // Job goes back to not being ready. Middle is still true, so use that alarm.
+ doReturn(false).when(mTimeController)
+ .wouldBeReadyWithConstraintLocked(eq(jobEarliest), anyInt());
+ mTimeController.evaluateStateLocked(jobEarliest);
+ inOrder.verify(mAlarmManager, times(1))
+ .set(anyInt(), eq(now + 30 * MINUTE_IN_MILLIS), anyLong(), anyLong(),
+ eq(TAG_DEADLINE), any(), any(), any());
+ // Turn middle off. Latest is true, so use that alarm.
+ doReturn(false).when(mTimeController)
+ .wouldBeReadyWithConstraintLocked(eq(jobMiddle), anyInt());
+ mTimeController.evaluateStateLocked(jobMiddle);
+ inOrder.verify(mAlarmManager, times(1))
+ .set(anyInt(), eq(now + HOUR_IN_MILLIS), anyLong(), anyLong(),
+ eq(TAG_DEADLINE), any(), any(), any());
+ }
+}
diff --git a/telephony/java/android/telephony/NetworkService.java b/telephony/java/android/telephony/NetworkService.java
index 4354314..4bca404 100644
--- a/telephony/java/android/telephony/NetworkService.java
+++ b/telephony/java/android/telephony/NetworkService.java
@@ -16,7 +16,6 @@
package android.telephony;
-import android.annotation.CallSuper;
import android.annotation.SystemApi;
import android.app.Service;
import android.content.Intent;
@@ -53,7 +52,6 @@
private final String TAG = NetworkService.class.getSimpleName();
public static final String NETWORK_SERVICE_INTERFACE = "android.telephony.NetworkService";
- public static final String NETWORK_SERVICE_EXTRA_SLOT_ID = "android.telephony.extra.SLOT_ID";
private static final int NETWORK_SERVICE_CREATE_NETWORK_SERVICE_PROVIDER = 1;
private static final int NETWORK_SERVICE_REMOVE_NETWORK_SERVICE_PROVIDER = 2;
@@ -81,7 +79,7 @@
* must extend this class to support network connection. Note that each instance of network
* service is associated with one physical SIM slot.
*/
- public class NetworkServiceProvider {
+ public abstract class NetworkServiceProvider implements AutoCloseable {
private final int mSlotId;
private final List<INetworkServiceCallback>
@@ -137,12 +135,12 @@
}
/**
- * Called when the instance of network service is destroyed (e.g. got unbind or binder died).
+ * Called when the instance of network service is destroyed (e.g. got unbind or binder died)
+ * or when the network service provider is removed. The extended class should implement this
+ * method to perform cleanup works.
*/
- @CallSuper
- protected void onDestroy() {
- mNetworkRegistrationStateChangedCallbacks.clear();
- }
+ @Override
+ public abstract void close();
}
private class NetworkServiceHandler extends Handler {
@@ -168,7 +166,7 @@
case NETWORK_SERVICE_REMOVE_NETWORK_SERVICE_PROVIDER:
// If the service provider doesn't exist yet, we try to create it.
if (serviceProvider != null) {
- serviceProvider.onDestroy();
+ serviceProvider.close();
mServiceMap.remove(slotId);
}
break;
@@ -176,7 +174,7 @@
for (int i = 0; i < mServiceMap.size(); i++) {
serviceProvider = mServiceMap.get(i);
if (serviceProvider != null) {
- serviceProvider.onDestroy();
+ serviceProvider.close();
}
}
mServiceMap.clear();
diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java
index 387453f..8c65eb6 100644
--- a/telephony/java/android/telephony/SubscriptionManager.java
+++ b/telephony/java/android/telephony/SubscriptionManager.java
@@ -572,7 +572,6 @@
* TelephonyProvider column name for whether a subscription is opportunistic, that is,
* whether the network it connects to is limited in functionality or coverage.
* For example, CBRS.
- * IS_EMBEDDED should always be true.
* <p>Type: INTEGER (int), 1 for opportunistic or 0 for non-opportunistic.
* @hide
*/
@@ -2379,18 +2378,32 @@
}
/**
- * Set opportunistic by simInfo index
+ * Set whether a subscription is opportunistic, that is, whether the network it connects
+ * to has limited coverage. For example, CBRS. Setting a subscription opportunistic has
+ * following impacts:
+ * 1) Even if it's active, it will be dormant most of the time. The modem will not try
+ * to scan or camp until it knows an available network is nearby to save power.
+ * 2) Telephony relies on system app or carrier input to notify nearby available networks.
+ * See {@link TelephonyManager#updateAvailableNetworks(List)} for more information.
+ * 3) In multi-SIM devices, when the network is nearby and camped, system may automatically
+ * switch internet data between it and default data subscription, based on carrier
+ * recommendation and its signal strength and metered-ness, etc.
+ *
+ *
+ * Caller will either have {@link android.Manifest.permission#MODIFY_PHONE_STATE} or carrier
+ * privilege permission of the subscription.
*
* @param opportunistic whether it’s opportunistic subscription.
* @param subId the unique SubscriptionInfo index in database
- * @return the number of records updated
- * @hide
+ * @return {@code true} if the operation is succeed, {@code false} otherwise.
*/
+ @SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges
@RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE)
- public int setOpportunistic(boolean opportunistic, int subId) {
+ public boolean setOpportunistic(boolean opportunistic, int subId) {
if (VDBG) logd("[setOpportunistic]+ opportunistic:" + opportunistic + " subId:" + subId);
return setSubscriptionPropertyHelper(subId, "setOpportunistic",
- (iSub)-> iSub.setOpportunistic(opportunistic, subId));
+ (iSub)-> iSub.setOpportunistic(
+ opportunistic, subId, mContext.getOpPackageName())) == 1;
}
/**
@@ -2510,18 +2523,26 @@
}
/**
- * Set metered by simInfo index
+ * Set if a subscription is metered or not. Similar to Wi-Fi, metered means
+ * user may be charged more if more data is used.
+ *
+ * By default all Cellular networks are considered metered. System or carrier privileged apps
+ * can set a subscription un-metered which will be considered when system switches data between
+ * primary subscription and opportunistic subscription.
+ *
+ * Caller will either have {@link android.Manifest.permission#MODIFY_PHONE_STATE} or carrier
+ * privilege permission of the subscription.
*
* @param isMetered whether it’s a metered subscription.
* @param subId the unique SubscriptionInfo index in database
- * @return the number of records updated
- * @hide
+ * @return {@code true} if the operation is succeed, {@code false} otherwise.
*/
+ @SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges
@RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE)
- public int setMetered(boolean isMetered, int subId) {
+ public boolean setMetered(boolean isMetered, int subId) {
if (VDBG) logd("[setIsMetered]+ isMetered:" + isMetered + " subId:" + subId);
return setSubscriptionPropertyHelper(subId, "setIsMetered",
- (iSub)-> iSub.setMetered(isMetered, subId));
+ (iSub)-> iSub.setMetered(isMetered, subId, mContext.getOpPackageName())) == 1;
}
/**
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index e2d03a0..e0632b1 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -6671,8 +6671,8 @@
try {
ITelephony telephony = getITelephony();
if (telephony != null) {
- return telephony.getCarrierPrivilegeStatus(subId) ==
- CARRIER_PRIVILEGE_STATUS_HAS_ACCESS;
+ return telephony.getCarrierPrivilegeStatus(subId)
+ == CARRIER_PRIVILEGE_STATUS_HAS_ACCESS;
}
} catch (RemoteException ex) {
Rlog.e(TAG, "hasCarrierPrivileges RemoteException", ex);
diff --git a/telephony/java/android/telephony/data/DataService.java b/telephony/java/android/telephony/data/DataService.java
index 1db5850..74d1e83 100644
--- a/telephony/java/android/telephony/data/DataService.java
+++ b/telephony/java/android/telephony/data/DataService.java
@@ -16,7 +16,6 @@
package android.telephony.data;
-import android.annotation.CallSuper;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -60,7 +59,6 @@
private static final String TAG = DataService.class.getSimpleName();
public static final String DATA_SERVICE_INTERFACE = "android.telephony.data.DataService";
- public static final String DATA_SERVICE_EXTRA_SLOT_ID = "android.telephony.data.extra.SLOT_ID";
/** {@hide} */
@IntDef(prefix = "REQUEST_REASON_", value = {
@@ -116,7 +114,7 @@
* must extend this class to support data connection. Note that each instance of data service
* provider is associated with one physical SIM slot.
*/
- public class DataServiceProvider {
+ public abstract class DataServiceProvider implements AutoCloseable {
private final int mSlotId;
@@ -250,12 +248,12 @@
}
/**
- * Called when the instance of data service is destroyed (e.g. got unbind or binder died).
+ * Called when the instance of data service is destroyed (e.g. got unbind or binder died)
+ * or when the data service provider is removed. The extended class should implement this
+ * method to perform cleanup works.
*/
- @CallSuper
- protected void onDestroy() {
- mDataCallListChangedCallbacks.clear();
- }
+ @Override
+ public abstract void close();
}
private static final class SetupDataCallRequest {
@@ -345,7 +343,7 @@
break;
case DATA_SERVICE_REMOVE_DATA_SERVICE_PROVIDER:
if (serviceProvider != null) {
- serviceProvider.onDestroy();
+ serviceProvider.close();
mServiceMap.remove(slotId);
}
break;
@@ -353,7 +351,7 @@
for (int i = 0; i < mServiceMap.size(); i++) {
serviceProvider = mServiceMap.get(i);
if (serviceProvider != null) {
- serviceProvider.onDestroy();
+ serviceProvider.close();
}
}
mServiceMap.clear();
diff --git a/telephony/java/android/telephony/data/QualifiedNetworksService.java b/telephony/java/android/telephony/data/QualifiedNetworksService.java
index 57d9cce..45b4849 100644
--- a/telephony/java/android/telephony/data/QualifiedNetworksService.java
+++ b/telephony/java/android/telephony/data/QualifiedNetworksService.java
@@ -151,7 +151,7 @@
/**
* Called when the qualified networks updater is removed. The extended class should
- * implement this method to perform clean up works.
+ * implement this method to perform cleanup works.
*/
@Override
public abstract void close();
diff --git a/telephony/java/com/android/internal/telephony/ISub.aidl b/telephony/java/com/android/internal/telephony/ISub.aidl
index 65d1a920..65eedb8 100755
--- a/telephony/java/com/android/internal/telephony/ISub.aidl
+++ b/telephony/java/com/android/internal/telephony/ISub.aidl
@@ -162,7 +162,7 @@
* @param subId the unique SubscriptionInfo index in database
* @return the number of records updated
*/
- int setOpportunistic(boolean opportunistic, int subId);
+ int setOpportunistic(boolean opportunistic, int subId, String callingPackage);
/**
* Inform SubscriptionManager that subscriptions in the list are bundled
@@ -190,7 +190,7 @@
* @param subId the unique SubscriptionInfo index in database
* @return the number of records updated
*/
- int setMetered(boolean isMetered, int subId);
+ int setMetered(boolean isMetered, int subId, String callingPackage);
/**
* Set which subscription is preferred for cellular data. It's
diff --git a/wifi/java/android/net/wifi/aware/DiscoverySession.java b/wifi/java/android/net/wifi/aware/DiscoverySession.java
index 699f54c..a47e70b 100644
--- a/wifi/java/android/net/wifi/aware/DiscoverySession.java
+++ b/wifi/java/android/net/wifi/aware/DiscoverySession.java
@@ -34,11 +34,11 @@
* {@link PublishDiscoverySession} and {@link SubscribeDiscoverySession}. This
* class provides functionality common to both publish and subscribe discovery sessions:
* <ul>
- * <li>Sending messages: {@link #sendMessage(PeerHandle, int, byte[])} method.
- * <li>Creating a network-specifier when requesting a Aware connection:
- * {@link #createNetworkSpecifierOpen(PeerHandle)} or
- * {@link #createNetworkSpecifierPassphrase(PeerHandle, String)}.
+ * <li>Sending messages: {@link #sendMessage(PeerHandle, int, byte[])} method.
+ * <li>Creating a network-specifier when requesting a Aware connection using
+ * {@link WifiAwareManager.NetworkSpecifierBuilder}.
* </ul>
+ * <p>
* The {@link #close()} method must be called to destroy discovery sessions once they are
* no longer needed.
*/
@@ -270,6 +270,7 @@
* <p>
* To set up an encrypted link use the
* {@link #createNetworkSpecifierPassphrase(PeerHandle, String)} API.
+ * @deprecated Use the replacement {@link WifiAwareManager.NetworkSpecifierBuilder}.
*
* @param peerHandle The peer's handle obtained through
* {@link DiscoverySessionCallback#onServiceDiscovered(PeerHandle, byte[], java.util.List)}
@@ -284,6 +285,7 @@
* android.net.ConnectivityManager.NetworkCallback)}
* [or other varieties of that API].
*/
+ @Deprecated
public NetworkSpecifier createNetworkSpecifierOpen(@NonNull PeerHandle peerHandle) {
if (mTerminated) {
Log.w(TAG, "createNetworkSpecifierOpen: called on terminated session");
@@ -318,6 +320,7 @@
* <p>
* Note: per the Wi-Fi Aware specification the roles are fixed - a Subscriber is an INITIATOR
* and a Publisher is a RESPONDER.
+ * @deprecated Use the replacement {@link WifiAwareManager.NetworkSpecifierBuilder}.
*
* @param peerHandle The peer's handle obtained through
* {@link DiscoverySessionCallback#onServiceDiscovered(PeerHandle,
@@ -336,6 +339,7 @@
* android.net.ConnectivityManager.NetworkCallback)}
* [or other varieties of that API].
*/
+ @Deprecated
public NetworkSpecifier createNetworkSpecifierPassphrase(
@NonNull PeerHandle peerHandle, @NonNull String passphrase) {
if (!WifiAwareUtils.validatePassphrase(passphrase)) {
@@ -376,6 +380,7 @@
* <p>
* Note: per the Wi-Fi Aware specification the roles are fixed - a Subscriber is an INITIATOR
* and a Publisher is a RESPONDER.
+ * @deprecated Use the replacement {@link WifiAwareManager.NetworkSpecifierBuilder}.
*
* @param peerHandle The peer's handle obtained through
* {@link DiscoverySessionCallback#onServiceDiscovered(PeerHandle,
@@ -397,6 +402,7 @@
*
* @hide
*/
+ @Deprecated
@SystemApi
public NetworkSpecifier createNetworkSpecifierPmk(@NonNull PeerHandle peerHandle,
@NonNull byte[] pmk) {
diff --git a/wifi/java/android/net/wifi/aware/WifiAwareManager.java b/wifi/java/android/net/wifi/aware/WifiAwareManager.java
index 8529a89..26a6c08 100644
--- a/wifi/java/android/net/wifi/aware/WifiAwareManager.java
+++ b/wifi/java/android/net/wifi/aware/WifiAwareManager.java
@@ -21,6 +21,7 @@
import android.annotation.Nullable;
import android.annotation.SdkConstant;
import android.annotation.SdkConstant.SdkConstantType;
+import android.annotation.SystemApi;
import android.annotation.SystemService;
import android.content.Context;
import android.net.ConnectivityManager;
@@ -57,11 +58,7 @@
* {@link WifiAwareSession#subscribe(SubscribeConfig, DiscoverySessionCallback, Handler)}.
* <li>Create a Aware network specifier to be used with
* {@link ConnectivityManager#requestNetwork(NetworkRequest, ConnectivityManager.NetworkCallback)}
- * to set-up a Aware connection with a peer. Refer to
- * {@link DiscoverySession#createNetworkSpecifierOpen(PeerHandle)},
- * {@link DiscoverySession#createNetworkSpecifierPassphrase(PeerHandle, String)},
- * {@link WifiAwareSession#createNetworkSpecifierOpen(int, byte[])}, and
- * {@link WifiAwareSession#createNetworkSpecifierPassphrase(int, byte[], String)}.
+ * to set-up a Aware connection with a peer. Refer to {@link NetworkSpecifierBuilder}.
* </ul>
* <p>
* Aware may not be usable when Wi-Fi is disabled (and other conditions). To validate that
@@ -110,10 +107,7 @@
* <li>{@link NetworkRequest.Builder#addTransportType(int)} of
* {@link android.net.NetworkCapabilities#TRANSPORT_WIFI_AWARE}.
* <li>{@link NetworkRequest.Builder#setNetworkSpecifier(String)} using
- * {@link WifiAwareSession#createNetworkSpecifierOpen(int, byte[])},
- * {@link WifiAwareSession#createNetworkSpecifierPassphrase(int, byte[], String)},
- * {@link DiscoverySession#createNetworkSpecifierOpen(PeerHandle)}, or
- * {@link DiscoverySession#createNetworkSpecifierPassphrase(PeerHandle, String)}.
+ * {@link NetworkSpecifierBuilder}.
* </ul>
*/
@SystemService(Context.WIFI_AWARE_SERVICE)
@@ -145,8 +139,6 @@
* Connection creation role is that of INITIATOR. Used to create a network specifier string
* when requesting a Aware network.
*
- * @see DiscoverySession#createNetworkSpecifierOpen(PeerHandle)
- * @see DiscoverySession#createNetworkSpecifierPassphrase(PeerHandle, String)
* @see WifiAwareSession#createNetworkSpecifierOpen(int, byte[])
* @see WifiAwareSession#createNetworkSpecifierPassphrase(int, byte[], String)
*/
@@ -156,8 +148,6 @@
* Connection creation role is that of RESPONDER. Used to create a network specifier string
* when requesting a Aware network.
*
- * @see DiscoverySession#createNetworkSpecifierOpen(PeerHandle)
- * @see DiscoverySession#createNetworkSpecifierPassphrase(PeerHandle, String)
* @see WifiAwareSession#createNetworkSpecifierOpen(int, byte[])
* @see WifiAwareSession#createNetworkSpecifierPassphrase(int, byte[], String)
*/
@@ -415,6 +405,11 @@
+ ", passphrase=" + ((passphrase == null) ? "null" : "non-null"));
}
+ if (!WifiAwareUtils.isLegacyVersion(mContext, Build.VERSION_CODES.Q)) {
+ throw new UnsupportedOperationException(
+ "API not deprecated - use WifiAwareManager.NetworkSpecifierBuilder");
+ }
+
if (role != WIFI_AWARE_DATA_PATH_ROLE_INITIATOR
&& role != WIFI_AWARE_DATA_PATH_ROLE_RESPONDER) {
throw new IllegalArgumentException(
@@ -813,4 +808,135 @@
mOriginalCallback.onSessionTerminated();
}
}
+
+ /**
+ * A builder class for a Wi-Fi Aware network specifier to set up an Aware connection with a
+ * peer.
+ * <p>
+ * Note that all Wi-Fi Aware connection specifier objects must call the
+ * {@link NetworkSpecifierBuilder#setDiscoverySession(DiscoverySession)} to specify the context
+ * within which the connection is created, and
+ * {@link NetworkSpecifierBuilder#setPeerHandle(PeerHandle)} to specify the peer to which the
+ * connection is created.
+ */
+ public static class NetworkSpecifierBuilder {
+ private DiscoverySession mDiscoverySession;
+ private PeerHandle mPeerHandle;
+ private String mPskPassphrase;
+ private byte[] mPmk;
+
+ /**
+ * Configure the {@link PublishDiscoverySession} or {@link SubscribeDiscoverySession}
+ * discovery session in whose context the connection is created.
+ * <p>
+ * Note: this method must be called for any connection request!
+ *
+ * @param discoverySession A Wi-Fi Aware discovery session.
+ * @return the current {@link NetworkSpecifierBuilder} builder, enabling chaining of builder
+ * methods.
+ */
+ public @NonNull NetworkSpecifierBuilder setDiscoverySession(
+ @NonNull DiscoverySession discoverySession) {
+ if (discoverySession == null) {
+ throw new IllegalArgumentException("Non-null discoverySession required");
+ }
+ mDiscoverySession = discoverySession;
+ return this;
+ }
+
+ /**
+ * Configure the {@link PeerHandle} of the peer to which the Wi-Fi Aware connection is
+ * requested. The peer is discovered through Wi-Fi Aware discovery,
+ * <p>
+ * Note: this method must be called for any connection request!
+ *
+ * @param peerHandle The peer's handle obtained through
+ * {@link DiscoverySessionCallback#onServiceDiscovered(PeerHandle, byte[], java.util.List)}
+ * or
+ * {@link DiscoverySessionCallback#onMessageReceived(PeerHandle, byte[])}.
+ * @return the current {@link NetworkSpecifierBuilder} builder, enabling chaining of builder
+ * methods.
+ */
+ public @NonNull NetworkSpecifierBuilder setPeerHandle(@NonNull PeerHandle peerHandle) {
+ if (peerHandle == null) {
+ throw new IllegalArgumentException("Non-null peerHandle required");
+ }
+ mPeerHandle = peerHandle;
+ return this;
+ }
+
+ /**
+ * Configure the PSK Passphrase for the Wi-Fi Aware connection being requested. This method
+ * is optional - if not called, then an Open (unencrypted) connection will be created.
+ *
+ * @param pskPassphrase The (optional) passphrase to be used to encrypt the link.
+ * @return the current {@link NetworkSpecifierBuilder} builder, enabling chaining of builder
+ * methods.
+ */
+ public @NonNull NetworkSpecifierBuilder setPskPassphrase(@NonNull String pskPassphrase) {
+ if (!WifiAwareUtils.validatePassphrase(pskPassphrase)) {
+ throw new IllegalArgumentException("Passphrase must meet length requirements");
+ }
+ mPskPassphrase = pskPassphrase;
+ return this;
+ }
+
+ /**
+ * Configure the PMK for the Wi-Fi Aware connection being requested. This method
+ * is optional - if not called, then an Open (unencrypted) connection will be created.
+ *
+ * @param pmk A PMK (pairwise master key, see IEEE 802.11i) specifying the key to use for
+ * encrypting the data-path. Use the {@link #setPskPassphrase(String)} to
+ * specify a Passphrase.
+ * @return the current {@link NetworkSpecifierBuilder} builder, enabling chaining of builder
+ * methods.
+ * @hide
+ */
+ @SystemApi
+ public @NonNull NetworkSpecifierBuilder setPmk(@NonNull byte[] pmk) {
+ if (!WifiAwareUtils.validatePmk(pmk)) {
+ throw new IllegalArgumentException("PMK must 32 bytes");
+ }
+ mPmk = pmk;
+ return this;
+ }
+
+ /**
+ * Create a {@link android.net.NetworkRequest.Builder#setNetworkSpecifier(NetworkSpecifier)}
+ * for a WiFi Aware connection (link) to the specified peer. The
+ * {@link android.net.NetworkRequest.Builder#addTransportType(int)} should be set to
+ * {@link android.net.NetworkCapabilities#TRANSPORT_WIFI_AWARE}.
+ * <p> The default builder constructor will initialize a NetworkSpecifier which requests an
+ * open (non-encrypted) link. To request an encrypted link use the
+ * {@link #setPskPassphrase(String)} builder method.
+ *
+ * @return A {@link NetworkSpecifier} to be used to construct
+ * {@link android.net.NetworkRequest.Builder#setNetworkSpecifier(NetworkSpecifier)} to pass
+ * to {@link android.net.ConnectivityManager#requestNetwork(android.net.NetworkRequest,
+ * android.net.ConnectivityManager.NetworkCallback)}
+ * [or other varieties of that API].
+ */
+ public @NonNull NetworkSpecifier build() {
+ if (mDiscoverySession == null) {
+ throw new IllegalStateException("Null discovery session!?");
+ }
+ if (mPskPassphrase != null & mPmk != null) {
+ throw new IllegalStateException(
+ "Can only specify a Passphrase or a PMK - not both!");
+ }
+
+ int role = mDiscoverySession instanceof SubscribeDiscoverySession
+ ? WifiAwareManager.WIFI_AWARE_DATA_PATH_ROLE_INITIATOR
+ : WifiAwareManager.WIFI_AWARE_DATA_PATH_ROLE_RESPONDER;
+
+ if (role == WIFI_AWARE_DATA_PATH_ROLE_INITIATOR && mPeerHandle == null) {
+ throw new IllegalStateException("Null peerHandle!?");
+ }
+
+ return new WifiAwareNetworkSpecifier(
+ WifiAwareNetworkSpecifier.NETWORK_SPECIFIER_TYPE_IB, role,
+ mDiscoverySession.mClientId, mDiscoverySession.mSessionId, mPeerHandle.peerId,
+ null, mPmk, mPskPassphrase, Process.myUid());
+ }
+ }
}
diff --git a/wifi/java/android/net/wifi/aware/WifiAwareNetworkInfo.java b/wifi/java/android/net/wifi/aware/WifiAwareNetworkInfo.java
new file mode 100644
index 0000000..0f29e08
--- /dev/null
+++ b/wifi/java/android/net/wifi/aware/WifiAwareNetworkInfo.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.wifi.aware;
+
+import android.annotation.Nullable;
+import android.net.NetworkCapabilities;
+import android.net.TransportInfo;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.net.Inet6Address;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.util.Objects;
+
+/**
+ * Wi-Fi Aware-specific network information. The information can be extracted from the
+ * {@link android.net.NetworkCapabilities} of the network using
+ * {@link NetworkCapabilities#getTransportInfo()}.
+ * The {@link NetworkCapabilities} is provided by the connectivity service to apps, e.g. received
+ * through the
+ * {@link android.net.ConnectivityManager.NetworkCallback#onCapabilitiesChanged(android.net.Network,
+ * android.net.NetworkCapabilities)} callback.
+ * <p>
+ * The Wi-Fi Aware-specific network information include the peer's scoped link-local IPv6 address
+ * for the Wi-Fi Aware link. The scoped link-local IPv6 can then be used to create a
+ * {@link java.net.Socket} connection to the peer.
+ */
+public final class WifiAwareNetworkInfo implements TransportInfo, Parcelable {
+ private Inet6Address mIpv6Addr;
+
+ /** @hide */
+ public WifiAwareNetworkInfo(Inet6Address ipv6Addr) {
+ mIpv6Addr = ipv6Addr;
+ }
+
+ /**
+ * Get the scoped link-local IPv6 address of the Wi-Fi Aware peer (not of the local device!).
+ *
+ * @return An IPv6 address.
+ */
+ @Nullable
+ public Inet6Address getPeerIpv6Addr() {
+ return mIpv6Addr;
+ }
+
+ // parcelable methods
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeByteArray(mIpv6Addr.getAddress());
+ NetworkInterface ni = mIpv6Addr.getScopedInterface();
+ dest.writeString(ni == null ? null : ni.getName());
+ }
+
+ public static final Creator<WifiAwareNetworkInfo> CREATOR =
+ new Creator<WifiAwareNetworkInfo>() {
+ @Override
+ public WifiAwareNetworkInfo createFromParcel(Parcel in) {
+ Inet6Address ipv6Addr;
+ try {
+ byte[] addr = in.createByteArray();
+ String interfaceName = in.readString();
+ NetworkInterface ni = null;
+ if (interfaceName != null) {
+ try {
+ ni = NetworkInterface.getByName(interfaceName);
+ } catch (SocketException e) {
+ e.printStackTrace();
+ }
+ }
+ ipv6Addr = Inet6Address.getByAddress(null, addr, ni);
+ } catch (UnknownHostException e) {
+ e.printStackTrace();
+ return null;
+ }
+
+ return new WifiAwareNetworkInfo(ipv6Addr);
+ }
+
+ @Override
+ public WifiAwareNetworkInfo[] newArray(int size) {
+ return new WifiAwareNetworkInfo[size];
+ }
+ };
+
+
+ // object methods
+
+ @Override
+ public String toString() {
+ return new StringBuilder("AwareNetworkInfo: IPv6=").append(mIpv6Addr).toString();
+ }
+
+ /** @hide */
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+
+ if (!(obj instanceof WifiAwareNetworkInfo)) {
+ return false;
+ }
+
+ WifiAwareNetworkInfo lhs = (WifiAwareNetworkInfo) obj;
+ return Objects.equals(mIpv6Addr, lhs.mIpv6Addr);
+ }
+
+ /** @hide */
+ @Override
+ public int hashCode() {
+ return Objects.hash(mIpv6Addr);
+ }
+}
diff --git a/wifi/java/android/net/wifi/aware/WifiAwareSession.java b/wifi/java/android/net/wifi/aware/WifiAwareSession.java
index 3219653..5f8841c 100644
--- a/wifi/java/android/net/wifi/aware/WifiAwareSession.java
+++ b/wifi/java/android/net/wifi/aware/WifiAwareSession.java
@@ -213,7 +213,7 @@
* This API is targeted for applications which can obtain the peer MAC address using OOB
* (out-of-band) discovery. Aware discovery does not provide the MAC address of the peer -
* when using Aware discovery use the alternative network specifier method -
- * {@link DiscoverySession#createNetworkSpecifierOpen(PeerHandle)}.
+ * {@link android.net.wifi.aware.WifiAwareManager.NetworkSpecifierBuilder}.
* <p>
* To set up an encrypted link use the
* {@link #createNetworkSpecifierPassphrase(int, byte[], String)} API.
@@ -254,7 +254,7 @@
* This API is targeted for applications which can obtain the peer MAC address using OOB
* (out-of-band) discovery. Aware discovery does not provide the MAC address of the peer -
* when using Aware discovery use the alternative network specifier method -
- * {@link DiscoverySession#createNetworkSpecifierPassphrase(PeerHandle, String)}.
+ * {@link android.net.wifi.aware.WifiAwareManager.NetworkSpecifierBuilder}.
*
* @param role The role of this device:
* {@link WifiAwareManager#WIFI_AWARE_DATA_PATH_ROLE_INITIATOR} or
@@ -300,7 +300,7 @@
* This API is targeted for applications which can obtain the peer MAC address using OOB
* (out-of-band) discovery. Aware discovery does not provide the MAC address of the peer -
* when using Aware discovery use the alternative network specifier method -
- * {@link DiscoverySession#createNetworkSpecifierPassphrase(PeerHandle, String)}.
+ * {@link android.net.wifi.aware.WifiAwareManager.NetworkSpecifierBuilder}.
*
* @param role The role of this device:
* {@link WifiAwareManager#WIFI_AWARE_DATA_PATH_ROLE_INITIATOR} or
diff --git a/wifi/tests/src/android/net/wifi/aware/WifiAwareManagerTest.java b/wifi/tests/src/android/net/wifi/aware/WifiAwareManagerTest.java
index 272f727..45e1720 100644
--- a/wifi/tests/src/android/net/wifi/aware/WifiAwareManagerTest.java
+++ b/wifi/tests/src/android/net/wifi/aware/WifiAwareManagerTest.java
@@ -31,6 +31,7 @@
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
+import android.net.MacAddress;
import android.net.wifi.RttManager;
import android.os.Build;
import android.os.Handler;
@@ -50,6 +51,8 @@
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import java.net.Inet6Address;
+import java.net.UnknownHostException;
import java.util.List;
/**
@@ -105,7 +108,7 @@
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
- mockApplicationInfo.targetSdkVersion = Build.VERSION_CODES.P;
+ mockApplicationInfo.targetSdkVersion = Build.VERSION_CODES.Q;
when(mockPackageManager.getApplicationInfo(anyString(), anyInt())).thenReturn(
mockApplicationInfo);
when(mockContext.getOpPackageName()).thenReturn("XXX");
@@ -915,6 +918,8 @@
final ConfigRequest configRequest = new ConfigRequest.Builder().build();
final PublishConfig publishConfig = new PublishConfig.Builder().build();
+ mockApplicationInfo.targetSdkVersion = Build.VERSION_CODES.P;
+
ArgumentCaptor<WifiAwareSession> sessionCaptor = ArgumentCaptor.forClass(
WifiAwareSession.class);
ArgumentCaptor<IWifiAwareEventCallback> clientProxyCallback = ArgumentCaptor
@@ -948,6 +953,9 @@
WifiAwareNetworkSpecifier ns =
(WifiAwareNetworkSpecifier) publishSession.getValue().createNetworkSpecifierOpen(
peerHandle);
+ WifiAwareNetworkSpecifier nsb = (WifiAwareNetworkSpecifier) new WifiAwareManager
+ .NetworkSpecifierBuilder().setDiscoverySession(publishSession.getValue())
+ .setPeerHandle(peerHandle).build();
// validate format
collector.checkThat("role", role, equalTo(ns.role));
@@ -955,9 +963,18 @@
collector.checkThat("session_id", sessionId, equalTo(ns.sessionId));
collector.checkThat("peer_id", peerHandle.peerId, equalTo(ns.peerId));
+ collector.checkThat("role", role, equalTo(nsb.role));
+ collector.checkThat("client_id", clientId, equalTo(nsb.clientId));
+ collector.checkThat("session_id", sessionId, equalTo(nsb.sessionId));
+ collector.checkThat("peer_id", peerHandle.peerId, equalTo(nsb.peerId));
+
// (4) request an encrypted (PMK) network specifier from the session
ns = (WifiAwareNetworkSpecifier) publishSession.getValue().createNetworkSpecifierPmk(
peerHandle, pmk);
+ nsb =
+ (WifiAwareNetworkSpecifier) new WifiAwareManager.NetworkSpecifierBuilder()
+ .setDiscoverySession(
+ publishSession.getValue()).setPeerHandle(peerHandle).setPmk(pmk).build();
// validate format
collector.checkThat("role", role, equalTo(ns.role));
@@ -966,9 +983,18 @@
collector.checkThat("peer_id", peerHandle.peerId, equalTo(ns.peerId));
collector.checkThat("pmk", pmk , equalTo(ns.pmk));
+ collector.checkThat("role", role, equalTo(nsb.role));
+ collector.checkThat("client_id", clientId, equalTo(nsb.clientId));
+ collector.checkThat("session_id", sessionId, equalTo(nsb.sessionId));
+ collector.checkThat("peer_id", peerHandle.peerId, equalTo(nsb.peerId));
+ collector.checkThat("pmk", pmk , equalTo(nsb.pmk));
+
// (5) request an encrypted (Passphrase) network specifier from the session
ns = (WifiAwareNetworkSpecifier) publishSession.getValue().createNetworkSpecifierPassphrase(
peerHandle, passphrase);
+ nsb = (WifiAwareNetworkSpecifier) new WifiAwareManager.NetworkSpecifierBuilder()
+ .setDiscoverySession(publishSession.getValue()).setPeerHandle(peerHandle)
+ .setPskPassphrase(passphrase).build();
// validate format
collector.checkThat("role", role, equalTo(ns.role));
@@ -977,6 +1003,12 @@
collector.checkThat("peer_id", peerHandle.peerId, equalTo(ns.peerId));
collector.checkThat("passphrase", passphrase, equalTo(ns.passphrase));
+ collector.checkThat("role", role, equalTo(nsb.role));
+ collector.checkThat("client_id", clientId, equalTo(nsb.clientId));
+ collector.checkThat("session_id", sessionId, equalTo(nsb.sessionId));
+ collector.checkThat("peer_id", peerHandle.peerId, equalTo(nsb.peerId));
+ collector.checkThat("passphrase", passphrase, equalTo(nsb.passphrase));
+
verifyNoMoreInteractions(mockCallback, mockSessionCallback, mockAwareService,
mockPublishSession, mockRttListener);
}
@@ -1048,7 +1080,7 @@
*/
@Test(expected = IllegalArgumentException.class)
public void testNetworkSpecifierWithClientNullPmk() throws Exception {
- executeNetworkSpecifierWithClient(new PeerHandle(123412), true, null, null);
+ executeNetworkSpecifierWithClient(new PeerHandle(123412), true, null, null, false);
}
/**
@@ -1056,7 +1088,7 @@
*/
@Test(expected = IllegalArgumentException.class)
public void testNetworkSpecifierWithClientIncorrectLengthPmk() throws Exception {
- executeNetworkSpecifierWithClient(new PeerHandle(123412), true, PMK_INVALID, null);
+ executeNetworkSpecifierWithClient(new PeerHandle(123412), true, PMK_INVALID, null, false);
}
/**
@@ -1064,7 +1096,7 @@
*/
@Test(expected = IllegalArgumentException.class)
public void testNetworkSpecifierWithClientNullPassphrase() throws Exception {
- executeNetworkSpecifierWithClient(new PeerHandle(123412), false, null, null);
+ executeNetworkSpecifierWithClient(new PeerHandle(123412), false, null, null, false);
}
/**
@@ -1073,7 +1105,7 @@
@Test(expected = IllegalArgumentException.class)
public void testNetworkSpecifierWithClientTooShortPassphrase() throws Exception {
executeNetworkSpecifierWithClient(new PeerHandle(123412), false, null,
- PASSPHRASE_TOO_SHORT);
+ PASSPHRASE_TOO_SHORT, false);
}
/**
@@ -1081,7 +1113,8 @@
*/
@Test(expected = IllegalArgumentException.class)
public void testNetworkSpecifierWithClientTooLongPassphrase() throws Exception {
- executeNetworkSpecifierWithClient(new PeerHandle(123412), false, null, PASSPHRASE_TOO_LONG);
+ executeNetworkSpecifierWithClient(new PeerHandle(123412), false, null, PASSPHRASE_TOO_LONG,
+ false);
}
/**
@@ -1089,7 +1122,8 @@
*/
@Test(expected = IllegalArgumentException.class)
public void testNetworkSpecifierWithClientNullPeer() throws Exception {
- executeNetworkSpecifierWithClient(null, false, null, PASSPHRASE_VALID);
+ mockApplicationInfo.targetSdkVersion = Build.VERSION_CODES.P;
+ executeNetworkSpecifierWithClient(null, false, null, PASSPHRASE_VALID, false);
}
/**
@@ -1098,11 +1132,75 @@
@Test
public void testNetworkSpecifierWithClientNullPeerLegacyApi() throws Exception {
mockApplicationInfo.targetSdkVersion = Build.VERSION_CODES.O;
- executeNetworkSpecifierWithClient(null, false, null, PASSPHRASE_VALID);
+ executeNetworkSpecifierWithClient(null, false, null, PASSPHRASE_VALID, false);
+ }
+
+ /**
+ * Validate that a null PMK triggers an exception.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testNetworkSpecifierWithClientNullPmkBuilder() throws Exception {
+ executeNetworkSpecifierWithClient(new PeerHandle(123412), true, null, null, true);
+ }
+
+ /**
+ * Validate that a non-32-bytes PMK triggers an exception.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testNetworkSpecifierWithClientIncorrectLengthPmkBuilder() throws Exception {
+ executeNetworkSpecifierWithClient(new PeerHandle(123412), true, PMK_INVALID, null, true);
+ }
+
+ /**
+ * Validate that a null Passphrase triggers an exception.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testNetworkSpecifierWithClientNullPassphraseBuilder() throws Exception {
+ executeNetworkSpecifierWithClient(new PeerHandle(123412), false, null, null, true);
+ }
+
+ /**
+ * Validate that a too short Passphrase triggers an exception.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testNetworkSpecifierWithClientTooShortPassphraseBuilder() throws Exception {
+ executeNetworkSpecifierWithClient(new PeerHandle(123412), false, null,
+ PASSPHRASE_TOO_SHORT, true);
+ }
+
+ /**
+ * Validate that a too long Passphrase triggers an exception.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testNetworkSpecifierWithClientTooLongPassphraseBuilder() throws Exception {
+ executeNetworkSpecifierWithClient(new PeerHandle(123412), false, null, PASSPHRASE_TOO_LONG,
+ true);
+ }
+
+ /**
+ * Validate that a null PeerHandle triggers an exception.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testNetworkSpecifierWithClientNullPeerBuilder() throws Exception {
+ executeNetworkSpecifierWithClient(null, false, null, PASSPHRASE_VALID, true);
+ }
+
+ /**
+ * Validate that a null PeerHandle does not trigger an exception for legacy API.
+ */
+ @Test
+ public void testNetworkSpecifierWithClientNullPeerLegacyApiBuilder() throws Exception {
+ mockApplicationInfo.targetSdkVersion = Build.VERSION_CODES.O;
+ executeNetworkSpecifierWithClient(null, false, null, PASSPHRASE_VALID, false);
+ }
+
+ @Test(expected = UnsupportedOperationException.class)
+ public void testNetworkSpecifierDeprecatedOnNewApi() throws Exception {
+ executeNetworkSpecifierWithClient(null, false, null, PASSPHRASE_VALID, false);
}
private void executeNetworkSpecifierWithClient(PeerHandle peerHandle, boolean doPmk, byte[] pmk,
- String passphrase) throws Exception {
+ String passphrase, boolean useBuilder) throws Exception {
final int clientId = 4565;
final int sessionId = 123;
final ConfigRequest configRequest = new ConfigRequest.Builder().build();
@@ -1139,9 +1237,20 @@
// (3) create network specifier
if (doPmk) {
- publishSession.getValue().createNetworkSpecifierPmk(peerHandle, pmk);
+ if (useBuilder) {
+ new WifiAwareManager.NetworkSpecifierBuilder().setDiscoverySession(
+ publishSession.getValue()).setPeerHandle(peerHandle).setPmk(pmk).build();
+ } else {
+ publishSession.getValue().createNetworkSpecifierPmk(peerHandle, pmk);
+ }
} else {
- publishSession.getValue().createNetworkSpecifierPassphrase(peerHandle, passphrase);
+ if (useBuilder) {
+ new WifiAwareManager.NetworkSpecifierBuilder().setDiscoverySession(
+ publishSession.getValue()).setPeerHandle(peerHandle).setPskPassphrase(
+ passphrase).build();
+ } else {
+ publishSession.getValue().createNetworkSpecifierPassphrase(peerHandle, passphrase);
+ }
}
}
@@ -1245,4 +1354,31 @@
sessionCaptor.getValue().createNetworkSpecifierPassphrase(role, someMac, passphrase);
}
}
+
+ // WifiAwareNetworkInfo tests
+
+ @Test
+ public void testWifiAwareNetworkCapabilitiesParcel() throws UnknownHostException {
+ final Inet6Address inet6 = MacAddress.fromString(
+ "11:22:33:44:55:66").getLinkLocalIpv6FromEui48Mac();
+ // note: dummy scope = 5
+ final Inet6Address inet6Scoped = Inet6Address.getByAddress(null, inet6.getAddress(), 5);
+
+ assertEquals(inet6Scoped.toString(), "/fe80::1322:33ff:fe44:5566%5");
+ WifiAwareNetworkInfo cap = new WifiAwareNetworkInfo(inet6Scoped);
+
+ Parcel parcelW = Parcel.obtain();
+ cap.writeToParcel(parcelW, 0);
+ byte[] bytes = parcelW.marshall();
+ parcelW.recycle();
+
+ Parcel parcelR = Parcel.obtain();
+ parcelR.unmarshall(bytes, 0, bytes.length);
+ parcelR.setDataPosition(0);
+ WifiAwareNetworkInfo rereadCap =
+ WifiAwareNetworkInfo.CREATOR.createFromParcel(parcelR);
+
+ assertEquals(cap.getPeerIpv6Addr().toString(), "/fe80::1322:33ff:fe44:5566%5");
+ assertEquals(cap.hashCode(), rereadCap.hashCode());
+ }
}