Inform apps of network changes.

Inform apps when they network JS thinks they should use changes so they
can attempt to switch over without tracking network changes themselves.

Bug: 152942222
Test: atest CtsJobSchedulerTestCases:ConnectivityConstraintTest
Change-Id: Ia6ddcba88ef89b217bfcaf5aeb734b33a59d35d3
This commit is contained in:
Kweku Adams
2023-01-09 18:07:16 +00:00
parent b3fb718c2d
commit f28d7efdb6
10 changed files with 153 additions and 9 deletions

View File

@@ -32,6 +32,8 @@ oneway interface IJobService {
/** Stop execution of application's job. */
@UnsupportedAppUsage
void stopJob(in JobParameters jobParams);
/** Inform the job of a change in the network it should use. */
void onNetworkChanged(in JobParameters jobParams);
/** Update JS of how much data has been downloaded. */
void getTransferredDownloadBytes(in JobParameters jobParams, in JobWorkItem jobWorkItem);
/** Update JS of how much data has been uploaded. */

View File

@@ -290,7 +290,8 @@ public class JobParameters implements Parcelable {
private final boolean mIsUserInitiated;
private final Uri[] mTriggeredContentUris;
private final String[] mTriggeredContentAuthorities;
private final Network network;
@Nullable
private Network mNetwork;
private int mStopReason = STOP_REASON_UNDEFINED;
private int mInternalStopReason = INTERNAL_STOP_REASON_UNKNOWN;
@@ -313,7 +314,7 @@ public class JobParameters implements Parcelable {
this.mIsUserInitiated = isUserInitiated;
this.mTriggeredContentUris = triggeredContentUris;
this.mTriggeredContentAuthorities = triggeredContentAuthorities;
this.network = network;
this.mNetwork = network;
this.mJobNamespace = namespace;
}
@@ -478,7 +479,7 @@ public class JobParameters implements Parcelable {
* @see JobInfo.Builder#setRequiredNetwork(NetworkRequest)
*/
public @Nullable Network getNetwork() {
return network;
return mNetwork;
}
/**
@@ -573,15 +574,20 @@ public class JobParameters implements Parcelable {
mTriggeredContentUris = in.createTypedArray(Uri.CREATOR);
mTriggeredContentAuthorities = in.createStringArray();
if (in.readInt() != 0) {
network = Network.CREATOR.createFromParcel(in);
mNetwork = Network.CREATOR.createFromParcel(in);
} else {
network = null;
mNetwork = null;
}
mStopReason = in.readInt();
mInternalStopReason = in.readInt();
debugStopReason = in.readString();
}
/** @hide */
public void setNetwork(@Nullable Network network) {
mNetwork = network;
}
/** @hide */
public void setStopReason(@StopReason int reason, int internalStopReason,
String debugStopReason) {
@@ -614,9 +620,9 @@ public class JobParameters implements Parcelable {
dest.writeBoolean(mIsUserInitiated);
dest.writeTypedArray(mTriggeredContentUris, flags);
dest.writeStringArray(mTriggeredContentAuthorities);
if (network != null) {
if (mNetwork != null) {
dest.writeInt(1);
network.writeToParcel(dest, flags);
mNetwork.writeToParcel(dest, flags);
} else {
dest.writeInt(0);
}

View File

@@ -27,6 +27,7 @@ import android.app.Service;
import android.compat.Compatibility;
import android.content.Intent;
import android.os.IBinder;
import android.util.Log;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -131,6 +132,11 @@ public abstract class JobService extends Service {
return JobService.this.getTransferredUploadBytes(params, item);
}
}
@Override
public void onNetworkChanged(@NonNull JobParameters params) {
JobService.this.onNetworkChanged(params);
}
};
}
return mEngine.getBinder();
@@ -231,6 +237,27 @@ public abstract class JobService extends Service {
*/
public abstract boolean onStopJob(JobParameters params);
/**
* This method is called that for a job that has a network constraint when the network
* to be used by the job changes. The new network object will be available via
* {@link JobParameters#getNetwork()}. Any network that results in this method call will
* match the job's requested network constraints.
*
* <p>
* For example, if a device is on a metered mobile network and then connects to an
* unmetered WiFi network, and the job has indicated that both networks satisfy its
* network constraint, then this method will be called to notify the job of the new
* unmetered WiFi network.
*
* @param params The parameters identifying this job, similar to what was supplied to the job in
* the {@link #onStartJob(JobParameters)} callback, but with an updated network.
* @see JobInfo.Builder#setRequiredNetwork(android.net.NetworkRequest)
* @see JobInfo.Builder#setRequiredNetworkType(int)
*/
public void onNetworkChanged(@NonNull JobParameters params) {
Log.w(TAG, "onNetworkChanged() not implemented. Must override in a subclass.");
}
/**
* Update the amount of data this job is estimated to transfer after the job has started.
*

View File

@@ -76,6 +76,8 @@ public abstract class JobServiceEngine {
private static final int MSG_UPDATE_ESTIMATED_NETWORK_BYTES = 6;
/** Message that the client wants to give JobScheduler a notification to tie to the job. */
private static final int MSG_SET_NOTIFICATION = 7;
/** Message that the network to use has changed. */
private static final int MSG_INFORM_OF_NETWORK_CHANGE = 8;
private final IJobService mBinder;
@@ -127,6 +129,16 @@ public abstract class JobServiceEngine {
}
}
@Override
public void onNetworkChanged(JobParameters jobParams) throws RemoteException {
JobServiceEngine service = mService.get();
if (service != null) {
service.mHandler.removeMessages(MSG_INFORM_OF_NETWORK_CHANGE);
service.mHandler.obtainMessage(MSG_INFORM_OF_NETWORK_CHANGE, jobParams)
.sendToTarget();
}
}
@Override
public void stopJob(JobParameters jobParams) throws RemoteException {
JobServiceEngine service = mService.get();
@@ -271,6 +283,16 @@ public abstract class JobServiceEngine {
args.recycle();
break;
}
case MSG_INFORM_OF_NETWORK_CHANGE: {
final JobParameters params = (JobParameters) msg.obj;
try {
JobServiceEngine.this.onNetworkChanged(params);
} catch (Exception e) {
Log.e(TAG, "Error while executing job: " + params.getJobId());
throw new RuntimeException(e);
}
break;
}
default:
Log.e(TAG, "Unrecognised message received.");
break;
@@ -385,6 +407,15 @@ public abstract class JobServiceEngine {
m.sendToTarget();
}
/**
* Engine's report that the network for the job has changed.
*
* @see JobService#onNetworkChanged(JobParameters)
*/
public void onNetworkChanged(@NonNull JobParameters params) {
Log.w(TAG, "onNetworkChanged() not implemented. Must override in a subclass.");
}
/**
* Engine's request to get how much data has been downloaded.
*

View File

@@ -1179,6 +1179,25 @@ class JobConcurrencyManager {
assignJobsToContextsLocked();
}
@Nullable
@GuardedBy("mLock")
JobServiceContext getRunningJobServiceContextLocked(JobStatus job) {
if (!mRunningJobs.contains(job)) {
return null;
}
for (int i = 0; i < mActiveServices.size(); i++) {
JobServiceContext jsc = mActiveServices.get(i);
final JobStatus executing = jsc.getRunningJobLocked();
if (executing == job) {
return jsc;
}
}
Slog.wtf(TAG, "Couldn't find running job on a context");
mRunningJobs.remove(job);
return null;
}
@GuardedBy("mLock")
boolean stopJobOnServiceContextLocked(JobStatus job,
@JobParameters.StopReason int reason, int internalReasonCode, String debugReason) {

View File

@@ -61,6 +61,7 @@ import android.content.pm.PackageManagerInternal;
import android.content.pm.ParceledListSlice;
import android.content.pm.ProviderInfo;
import android.content.pm.ServiceInfo;
import android.net.Network;
import android.net.Uri;
import android.os.BatteryManager;
import android.os.BatteryManagerInternal;
@@ -1939,6 +1940,17 @@ public class JobSchedulerService extends com.android.server.SystemService
}
}
@Override
public void onNetworkChanged(JobStatus jobStatus, Network newNetwork) {
synchronized (mLock) {
final JobServiceContext jsc =
mConcurrencyManager.getRunningJobServiceContextLocked(jobStatus);
if (jsc != null) {
jsc.informOfNetworkChangeLocked(newNetwork);
}
}
}
@Override
public void onRestrictedBucketChanged(List<JobStatus> jobs) {
final int len = jobs.size();

View File

@@ -36,6 +36,7 @@ import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.net.Network;
import android.net.Uri;
import android.os.Binder;
import android.os.Build;
@@ -187,6 +188,8 @@ public final class JobServiceContext implements ServiceConnection {
private int mPendingInternalStopReason;
private String mPendingDebugStopReason;
private Network mPendingNetworkChange;
// Debugging: reason this job was last stopped.
public String mStoppedReason;
@@ -292,6 +295,7 @@ public final class JobServiceContext implements ServiceConnection {
mRunningJob = job;
mRunningJobWorkType = workType;
mRunningCallback = new JobCallback();
mPendingNetworkChange = null;
final boolean isDeadlineExpired =
job.hasDeadlineConstraint() &&
(job.getLatestRunTimeElapsed() < sElapsedRealtimeClock.millis());
@@ -515,6 +519,28 @@ public final class JobServiceContext implements ServiceConnection {
return Math.max(0, mExecutionStartTimeElapsed + mMinExecutionGuaranteeMillis - nowElapsed);
}
void informOfNetworkChangeLocked(Network newNetwork) {
if (mVerb != VERB_EXECUTING) {
Slog.w(TAG, "Sending onNetworkChanged for a job that isn't started. " + mRunningJob);
if (mVerb == VERB_BINDING || mVerb == VERB_STARTING) {
// The network changed before the job has fully started. Hold the change push
// until the job has started executing.
mPendingNetworkChange = newNetwork;
}
return;
}
try {
mParams.setNetwork(newNetwork);
mPendingNetworkChange = null;
service.onNetworkChanged(mParams);
} catch (RemoteException e) {
Slog.e(TAG, "Error sending onNetworkChanged to client.", e);
// The job's host app apparently crashed during the job, so we should reschedule.
closeAndCleanupJobLocked(/* reschedule */ true,
"host crashed when trying to inform of network change");
}
}
boolean isWithinExecutionGuaranteeTime() {
return sElapsedRealtimeClock.millis()
< mExecutionStartTimeElapsed + mMinExecutionGuaranteeMillis;
@@ -972,6 +998,10 @@ public final class JobServiceContext implements ServiceConnection {
return;
}
scheduleOpTimeOutLocked();
if (mPendingNetworkChange != null
&& !Objects.equals(mParams.getNetwork(), mPendingNetworkChange)) {
informOfNetworkChangeLocked(mPendingNetworkChange);
}
if (mRunningJob.isUserVisibleJob()) {
mService.informObserversOfUserVisibleJobChange(this, mRunningJob, true);
}
@@ -1225,6 +1255,7 @@ public final class JobServiceContext implements ServiceConnection {
mPendingStopReason = JobParameters.STOP_REASON_UNDEFINED;
mPendingInternalStopReason = 0;
mPendingDebugStopReason = null;
mPendingNetworkChange = null;
removeOpTimeOutLocked();
if (completedJob.isUserVisibleJob()) {
mService.informObserversOfUserVisibleJobChange(this, completedJob, false);

View File

@@ -18,6 +18,7 @@ package com.android.server.job;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.net.Network;
import android.util.ArraySet;
import com.android.server.job.controllers.JobStatus;
@@ -57,6 +58,8 @@ public interface StateChangedListener {
public void onDeviceIdleStateChanged(boolean deviceIdle);
void onNetworkChanged(JobStatus jobStatus, Network newNetwork);
/**
* Called when these jobs are added or removed from the
* {@link android.app.usage.UsageStatsManager#STANDBY_BUCKET_RESTRICTED} bucket.

View File

@@ -1125,6 +1125,17 @@ public final class ConnectivityController extends RestrictingController implemen
mFlexibilityController.isFlexibilitySatisfiedLocked(jobStatus));
}
// Try to handle network transitions in a reasonable manner. See the lengthy note inside
// UidDefaultNetworkCallback for more details.
if (!changed && satisfied && jobStatus.network != null
&& mService.isCurrentlyRunningLocked(jobStatus)) {
// The job's connectivity constraint continues to be satisfied even though the network
// has changed.
// Inform the job of the new network so that it can attempt to switch over. This is the
// ideal behavior for certain transitions such as going from a metered network to an
// unmetered network.
mStateChangedListener.onNetworkChanged(jobStatus, network);
}
// Pass along the evaluated network for job to use; prevents race
// conditions as default routes change over time, and opens the door to
@@ -1419,8 +1430,8 @@ public final class ConnectivityController extends RestrictingController implemen
// the onBlockedStatusChanged() call, we re-evaluate the job, but keep it running
// (assuming the new network satisfies constraints). The app continues to use the old
// network (if they use the network object provided through JobParameters.getNetwork())
// because we don't notify them of the default network change. If the old network no
// longer satisfies requested constraints, then we have a problem. Depending on the order
// because we don't notify them of the default network change. If the old network later
// stops satisfying requested constraints, then we have a problem. Depending on the order
// of calls, if the per-UID callback gets notified of the network change before the
// general callback gets notified of the capabilities change, then the job's network
// object will point to the new network and we won't stop the job, even though we told it

View File

@@ -8626,6 +8626,7 @@ package android.app.job {
ctor public JobService();
method public final void jobFinished(android.app.job.JobParameters, boolean);
method public final android.os.IBinder onBind(android.content.Intent);
method public void onNetworkChanged(@NonNull android.app.job.JobParameters);
method public abstract boolean onStartJob(android.app.job.JobParameters);
method public abstract boolean onStopJob(android.app.job.JobParameters);
method public final void setNotification(@NonNull android.app.job.JobParameters, int, @NonNull android.app.Notification, int);
@@ -8640,6 +8641,7 @@ package android.app.job {
ctor public JobServiceEngine(android.app.Service);
method public final android.os.IBinder getBinder();
method public void jobFinished(android.app.job.JobParameters, boolean);
method public void onNetworkChanged(@NonNull android.app.job.JobParameters);
method public abstract boolean onStartJob(android.app.job.JobParameters);
method public abstract boolean onStopJob(android.app.job.JobParameters);
method public void setNotification(@NonNull android.app.job.JobParameters, int, @NonNull android.app.Notification, int);