diff options
| -rw-r--r-- | api/current.txt | 5 | ||||
| -rw-r--r-- | api/system-current.txt | 5 | ||||
| -rw-r--r-- | api/test-current.txt | 5 | ||||
| -rw-r--r-- | core/java/android/app/job/JobInfo.java | 67 | ||||
| -rw-r--r-- | core/java/android/app/job/JobWorkItem.java | 35 | ||||
| -rw-r--r-- | services/core/java/com/android/server/job/JobServiceContext.java | 2 | ||||
| -rw-r--r-- | services/core/java/com/android/server/job/controllers/ConnectivityController.java | 57 | ||||
| -rw-r--r-- | services/core/java/com/android/server/job/controllers/JobStatus.java | 43 |
8 files changed, 215 insertions, 4 deletions
diff --git a/api/current.txt b/api/current.txt index bdfa68ef6615..2fd75c7c5975 100644 --- a/api/current.txt +++ b/api/current.txt @@ -6834,6 +6834,7 @@ package android.app.job { method public int getBackoffPolicy(); method public android.content.ClipData getClipData(); method public int getClipGrantFlags(); + method public long getEstimatedNetworkBytes(); method public android.os.PersistableBundle getExtras(); method public long getFlexMillis(); method public int getId(); @@ -6861,6 +6862,7 @@ package android.app.job { field public static final android.os.Parcelable.Creator<android.app.job.JobInfo> CREATOR; field public static final long DEFAULT_INITIAL_BACKOFF_MILLIS = 30000L; // 0x7530L field public static final long MAX_BACKOFF_DELAY_MILLIS = 18000000L; // 0x112a880L + field public static final int NETWORK_BYTES_UNKNOWN = -1; // 0xffffffff field public static final int NETWORK_TYPE_ANY = 1; // 0x1 field public static final int NETWORK_TYPE_METERED = 4; // 0x4 field public static final int NETWORK_TYPE_NONE = 0; // 0x0 @@ -6874,6 +6876,7 @@ package android.app.job { method public android.app.job.JobInfo build(); method public android.app.job.JobInfo.Builder setBackoffCriteria(long, int); method public android.app.job.JobInfo.Builder setClipData(android.content.ClipData, int); + method public android.app.job.JobInfo.Builder setEstimatedNetworkBytes(long); method public android.app.job.JobInfo.Builder setExtras(android.os.PersistableBundle); method public android.app.job.JobInfo.Builder setMinimumLatency(long); method public android.app.job.JobInfo.Builder setOverrideDeadline(long); @@ -6948,8 +6951,10 @@ package android.app.job { public final class JobWorkItem implements android.os.Parcelable { ctor public JobWorkItem(android.content.Intent); + ctor public JobWorkItem(android.content.Intent, long); method public int describeContents(); method public int getDeliveryCount(); + method public long getEstimatedNetworkBytes(); method public android.content.Intent getIntent(); method public void writeToParcel(android.os.Parcel, int); field public static final android.os.Parcelable.Creator<android.app.job.JobWorkItem> CREATOR; diff --git a/api/system-current.txt b/api/system-current.txt index e3162e3f0881..cf92177ded0b 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -7276,6 +7276,7 @@ package android.app.job { method public int getBackoffPolicy(); method public android.content.ClipData getClipData(); method public int getClipGrantFlags(); + method public long getEstimatedNetworkBytes(); method public android.os.PersistableBundle getExtras(); method public long getFlexMillis(); method public int getId(); @@ -7303,6 +7304,7 @@ package android.app.job { field public static final android.os.Parcelable.Creator<android.app.job.JobInfo> CREATOR; field public static final long DEFAULT_INITIAL_BACKOFF_MILLIS = 30000L; // 0x7530L field public static final long MAX_BACKOFF_DELAY_MILLIS = 18000000L; // 0x112a880L + field public static final int NETWORK_BYTES_UNKNOWN = -1; // 0xffffffff field public static final int NETWORK_TYPE_ANY = 1; // 0x1 field public static final int NETWORK_TYPE_METERED = 4; // 0x4 field public static final int NETWORK_TYPE_NONE = 0; // 0x0 @@ -7316,6 +7318,7 @@ package android.app.job { method public android.app.job.JobInfo build(); method public android.app.job.JobInfo.Builder setBackoffCriteria(long, int); method public android.app.job.JobInfo.Builder setClipData(android.content.ClipData, int); + method public android.app.job.JobInfo.Builder setEstimatedNetworkBytes(long); method public android.app.job.JobInfo.Builder setExtras(android.os.PersistableBundle); method public android.app.job.JobInfo.Builder setMinimumLatency(long); method public android.app.job.JobInfo.Builder setOverrideDeadline(long); @@ -7391,8 +7394,10 @@ package android.app.job { public final class JobWorkItem implements android.os.Parcelable { ctor public JobWorkItem(android.content.Intent); + ctor public JobWorkItem(android.content.Intent, long); method public int describeContents(); method public int getDeliveryCount(); + method public long getEstimatedNetworkBytes(); method public android.content.Intent getIntent(); method public void writeToParcel(android.os.Parcel, int); field public static final android.os.Parcelable.Creator<android.app.job.JobWorkItem> CREATOR; diff --git a/api/test-current.txt b/api/test-current.txt index a62399afd51c..ae7f4d665004 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -6905,6 +6905,7 @@ package android.app.job { method public int getBackoffPolicy(); method public android.content.ClipData getClipData(); method public int getClipGrantFlags(); + method public long getEstimatedNetworkBytes(); method public android.os.PersistableBundle getExtras(); method public long getFlexMillis(); method public int getId(); @@ -6932,6 +6933,7 @@ package android.app.job { field public static final android.os.Parcelable.Creator<android.app.job.JobInfo> CREATOR; field public static final long DEFAULT_INITIAL_BACKOFF_MILLIS = 30000L; // 0x7530L field public static final long MAX_BACKOFF_DELAY_MILLIS = 18000000L; // 0x112a880L + field public static final int NETWORK_BYTES_UNKNOWN = -1; // 0xffffffff field public static final int NETWORK_TYPE_ANY = 1; // 0x1 field public static final int NETWORK_TYPE_METERED = 4; // 0x4 field public static final int NETWORK_TYPE_NONE = 0; // 0x0 @@ -6945,6 +6947,7 @@ package android.app.job { method public android.app.job.JobInfo build(); method public android.app.job.JobInfo.Builder setBackoffCriteria(long, int); method public android.app.job.JobInfo.Builder setClipData(android.content.ClipData, int); + method public android.app.job.JobInfo.Builder setEstimatedNetworkBytes(long); method public android.app.job.JobInfo.Builder setExtras(android.os.PersistableBundle); method public android.app.job.JobInfo.Builder setMinimumLatency(long); method public android.app.job.JobInfo.Builder setOverrideDeadline(long); @@ -7019,8 +7022,10 @@ package android.app.job { public final class JobWorkItem implements android.os.Parcelable { ctor public JobWorkItem(android.content.Intent); + ctor public JobWorkItem(android.content.Intent, long); method public int describeContents(); method public int getDeliveryCount(); + method public long getEstimatedNetworkBytes(); method public android.content.Intent getIntent(); method public void writeToParcel(android.os.Parcel, int); field public static final android.os.Parcelable.Creator<android.app.job.JobWorkItem> CREATOR; diff --git a/core/java/android/app/job/JobInfo.java b/core/java/android/app/job/JobInfo.java index 1cde73a0af61..b640bd5bee2a 100644 --- a/core/java/android/app/job/JobInfo.java +++ b/core/java/android/app/job/JobInfo.java @@ -18,6 +18,7 @@ package android.app.job; import static android.util.TimeUtils.formatDuration; +import android.annotation.BytesLong; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -71,6 +72,9 @@ public class JobInfo implements Parcelable { /** This job requires metered connectivity such as most cellular data networks. */ public static final int NETWORK_TYPE_METERED = 4; + /** Sentinel value indicating that bytes are unknown. */ + public static final int NETWORK_BYTES_UNKNOWN = -1; + /** * Amount of backoff a job has initially by default, in milliseconds. */ @@ -250,6 +254,7 @@ public class JobInfo implements Parcelable { private final boolean hasEarlyConstraint; private final boolean hasLateConstraint; private final int networkType; + private final long networkBytes; private final long minLatencyMillis; private final long maxExecutionDelayMillis; private final boolean isPeriodic; @@ -387,6 +392,18 @@ public class JobInfo implements Parcelable { } /** + * Return the estimated size of network traffic that will be performed by + * this job, in bytes. + * + * @return Estimated size of network traffic, or + * {@link #NETWORK_BYTES_UNKNOWN} when unknown. + * @see Builder#setEstimatedNetworkBytes(long) + */ + public @BytesLong long getEstimatedNetworkBytes() { + return networkBytes; + } + + /** * Set for a job that does not recur periodically, to specify a delay after which the job * will be eligible for execution. This value is not set if the job recurs periodically. */ @@ -524,6 +541,9 @@ public class JobInfo implements Parcelable { if (networkType != j.networkType) { return false; } + if (networkBytes != j.networkBytes) { + return false; + } if (minLatencyMillis != j.minLatencyMillis) { return false; } @@ -582,6 +602,7 @@ public class JobInfo implements Parcelable { hashCode = 31 * hashCode + Boolean.hashCode(hasEarlyConstraint); hashCode = 31 * hashCode + Boolean.hashCode(hasLateConstraint); hashCode = 31 * hashCode + networkType; + hashCode = 31 * hashCode + Long.hashCode(networkBytes); hashCode = 31 * hashCode + Long.hashCode(minLatencyMillis); hashCode = 31 * hashCode + Long.hashCode(maxExecutionDelayMillis); hashCode = 31 * hashCode + Boolean.hashCode(isPeriodic); @@ -612,6 +633,7 @@ public class JobInfo implements Parcelable { triggerContentUpdateDelay = in.readLong(); triggerContentMaxDelay = in.readLong(); networkType = in.readInt(); + networkBytes = in.readLong(); minLatencyMillis = in.readLong(); maxExecutionDelayMillis = in.readLong(); isPeriodic = in.readInt() == 1; @@ -640,6 +662,7 @@ public class JobInfo implements Parcelable { triggerContentUpdateDelay = b.mTriggerContentUpdateDelay; triggerContentMaxDelay = b.mTriggerContentMaxDelay; networkType = b.mNetworkType; + networkBytes = b.mNetworkBytes; minLatencyMillis = b.mMinLatencyMillis; maxExecutionDelayMillis = b.mMaxExecutionDelayMillis; isPeriodic = b.mIsPeriodic; @@ -677,6 +700,7 @@ public class JobInfo implements Parcelable { out.writeLong(triggerContentUpdateDelay); out.writeLong(triggerContentMaxDelay); out.writeInt(networkType); + out.writeLong(networkBytes); out.writeLong(minLatencyMillis); out.writeLong(maxExecutionDelayMillis); out.writeInt(isPeriodic ? 1 : 0); @@ -810,6 +834,7 @@ public class JobInfo implements Parcelable { // Requirements. private int mConstraintFlags; private int mNetworkType; + private long mNetworkBytes = NETWORK_BYTES_UNKNOWN; private ArrayList<TriggerContentUri> mTriggerContentUris; private long mTriggerContentUpdateDelay = -1; private long mTriggerContentMaxDelay = -1; @@ -931,6 +956,43 @@ public class JobInfo implements Parcelable { } /** + * Set the estimated size of network traffic that will be performed by + * this job, in bytes. + * <p> + * Apps are encouraged to provide values that are as accurate as + * possible, but when the exact size isn't available, an + * order-of-magnitude estimate can be provided instead. Here are some + * specific examples: + * <ul> + * <li>A job that is backing up a photo knows the exact size of that + * photo, so it should provide that size as the estimate. + * <li>A job that refreshes top news stories wouldn't know an exact + * size, but if the size is expected to be consistently around 100KB, it + * can provide that order-of-magnitude value as the estimate. + * <li>A job that synchronizes email could end up using an extreme range + * of data, from under 1KB when nothing has changed, to dozens of MB + * when there are new emails with attachments. Jobs that cannot provide + * reasonable estimates should leave this estimated value undefined. + * </ul> + * Note that the system may choose to delay jobs with large network + * usage estimates when the device has a poor network connection, in + * order to save battery. + * + * @param networkBytes The estimated size of network traffic that will + * be performed by this job, in bytes. This value only + * reflects the traffic that will be performed by the base + * job; if you're using {@link JobWorkItem} then you also + * need to define the network traffic used by each work item + * when constructing them. + * @see JobInfo#getEstimatedNetworkBytes() + * @see JobWorkItem#JobWorkItem(android.content.Intent, long) + */ + public Builder setEstimatedNetworkBytes(@BytesLong long networkBytes) { + mNetworkBytes = networkBytes; + return this; + } + + /** * Specify that to run this job, the device must be charging (or be a * non-battery-powered device connected to permanent power, such as Android TV * devices). This defaults to {@code false}. @@ -1156,6 +1218,11 @@ public class JobInfo implements Parcelable { 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 (mNetworkBytes > 0 && mNetworkType == NETWORK_TYPE_NONE) { + throw new IllegalArgumentException( + "Can't provide estimated network usage without requiring a network"); + } // Check that a deadline was not set on a periodic job. if (mIsPeriodic) { if (mMaxExecutionDelayMillis != 0L) { diff --git a/core/java/android/app/job/JobWorkItem.java b/core/java/android/app/job/JobWorkItem.java index 0eb0450e8f2a..1c46e8ecbe52 100644 --- a/core/java/android/app/job/JobWorkItem.java +++ b/core/java/android/app/job/JobWorkItem.java @@ -16,6 +16,7 @@ package android.app.job; +import android.annotation.BytesLong; import android.content.Intent; import android.os.Parcel; import android.os.Parcelable; @@ -27,6 +28,7 @@ import android.os.Parcelable; */ final public class JobWorkItem implements Parcelable { final Intent mIntent; + final long mNetworkBytes; int mDeliveryCount; int mWorkId; Object mGrants; @@ -39,6 +41,22 @@ final public class JobWorkItem implements Parcelable { */ public JobWorkItem(Intent intent) { mIntent = intent; + mNetworkBytes = JobInfo.NETWORK_BYTES_UNKNOWN; + } + + /** + * Create a new piece of work, which can be submitted to + * {@link JobScheduler#enqueue JobScheduler.enqueue}. + * + * @param intent The general Intent describing this work. + * @param networkBytes The estimated size of network traffic that will be + * performed by this job work item, in bytes. See + * {@link JobInfo.Builder#setEstimatedNetworkBytes(long)} for + * details about how to estimate. + */ + public JobWorkItem(Intent intent, @BytesLong long networkBytes) { + mIntent = intent; + mNetworkBytes = networkBytes; } /** @@ -49,6 +67,17 @@ final public class JobWorkItem implements Parcelable { } /** + * Return the estimated size of network traffic that will be performed by + * this job work item, in bytes. + * + * @return estimated size, or {@link JobInfo#NETWORK_BYTES_UNKNOWN} when + * unknown. + */ + public @BytesLong long getEstimatedNetworkBytes() { + return mNetworkBytes; + } + + /** * Return the count of the number of times this work item has been delivered * to the job. The value will be > 1 if it has been redelivered because the job * was stopped or crashed while it had previously been delivered but before the @@ -99,6 +128,10 @@ final public class JobWorkItem implements Parcelable { sb.append(mWorkId); sb.append(" intent="); sb.append(mIntent); + if (mNetworkBytes != JobInfo.NETWORK_BYTES_UNKNOWN) { + sb.append(" networkBytes="); + sb.append(mNetworkBytes); + } if (mDeliveryCount != 0) { sb.append(" dcount="); sb.append(mDeliveryCount); @@ -118,6 +151,7 @@ final public class JobWorkItem implements Parcelable { } else { out.writeInt(0); } + out.writeLong(mNetworkBytes); out.writeInt(mDeliveryCount); out.writeInt(mWorkId); } @@ -139,6 +173,7 @@ final public class JobWorkItem implements Parcelable { } else { mIntent = null; } + mNetworkBytes = in.readLong(); mDeliveryCount = in.readInt(); mWorkId = in.readInt(); } diff --git a/services/core/java/com/android/server/job/JobServiceContext.java b/services/core/java/com/android/server/job/JobServiceContext.java index ae01c433fc8d..a7e674b5a3a1 100644 --- a/services/core/java/com/android/server/job/JobServiceContext.java +++ b/services/core/java/com/android/server/job/JobServiceContext.java @@ -65,7 +65,7 @@ public final class JobServiceContext implements ServiceConnection { private static final boolean DEBUG = JobSchedulerService.DEBUG; private static final String TAG = "JobServiceContext"; /** Amount of time a job is allowed to execute for before being considered timed-out. */ - private static final long EXECUTING_TIMESLICE_MILLIS = 10 * 60 * 1000; // 10mins. + public static final long EXECUTING_TIMESLICE_MILLIS = 10 * 60 * 1000; // 10mins. /** Amount of time the JobScheduler waits for the initial service launch+bind. */ private static final long OP_BIND_TIMEOUT_MILLIS = 18 * 1000; /** Amount of time the JobScheduler will wait for a response from an app for a message. */ diff --git a/services/core/java/com/android/server/job/controllers/ConnectivityController.java b/services/core/java/com/android/server/job/controllers/ConnectivityController.java index c928c07be983..ddee345a1c42 100644 --- a/services/core/java/com/android/server/job/controllers/ConnectivityController.java +++ b/services/core/java/com/android/server/job/controllers/ConnectivityController.java @@ -25,13 +25,16 @@ import android.net.Network; import android.net.NetworkCapabilities; import android.net.NetworkInfo; import android.net.NetworkPolicyManager; +import android.net.TrafficStats; import android.os.Process; import android.os.UserHandle; +import android.text.format.DateUtils; import android.util.ArraySet; import android.util.Slog; import com.android.internal.annotations.GuardedBy; import com.android.server.job.JobSchedulerService; +import com.android.server.job.JobServiceContext; import com.android.server.job.StateChangedListener; import java.io.PrintWriter; @@ -99,18 +102,66 @@ public final class ConnectivityController extends StateController implements } } + /** + * Test to see if running the given job on the given network is sane. + * <p> + * For example, if a job is trying to send 10MB over a 128Kbps EDGE + * connection, it would take 10.4 minutes, and has no chance of succeeding + * before the job times out, so we'd be insane to try running it. + */ + private boolean isSane(JobStatus jobStatus, NetworkCapabilities capabilities) { + final long estimatedBytes = jobStatus.getEstimatedNetworkBytes(); + if (estimatedBytes == JobInfo.NETWORK_BYTES_UNKNOWN) { + // We don't know how large the job is; cross our fingers! + return true; + } + if (capabilities == null) { + // We don't know what the network is like; cross our fingers! + return true; + } + + // We don't ask developers to differentiate between upstream/downstream + // in their size estimates, so test against the slowest link direction. + final long downstream = capabilities.getLinkDownstreamBandwidthKbps(); + final long upstream = capabilities.getLinkUpstreamBandwidthKbps(); + final long slowest; + if (downstream > 0 && upstream > 0) { + slowest = Math.min(downstream, upstream); + } else if (downstream > 0) { + slowest = downstream; + } else if (upstream > 0) { + slowest = upstream; + } else { + // We don't know what the network is like; cross our fingers! + return true; + } + + final long estimatedMillis = ((estimatedBytes * DateUtils.SECOND_IN_MILLIS) + / (slowest * TrafficStats.KB_IN_BYTES / 8)); + if (estimatedMillis > JobServiceContext.EXECUTING_TIMESLICE_MILLIS) { + // If we'd never finish before the timeout, we'd be insane! + Slog.w(TAG, "Estimated " + estimatedBytes + " bytes over " + slowest + + " kbps network would take " + estimatedMillis + "ms; that's insane!"); + return false; + } else { + return true; + } + } + private boolean updateConstraintsSatisfied(JobStatus jobStatus) { final int jobUid = jobStatus.getSourceUid(); final boolean ignoreBlocked = (jobStatus.getFlags() & JobInfo.FLAG_WILL_BE_FOREGROUND) != 0; - final NetworkInfo info = mConnManager.getActiveNetworkInfoForUid(jobUid, ignoreBlocked); final Network network = mConnManager.getActiveNetworkForUid(jobUid, ignoreBlocked); + final NetworkInfo info = mConnManager.getNetworkInfoForUid(network, jobUid, ignoreBlocked); + final NetworkCapabilities capabilities = (network != null) ? mConnManager.getNetworkCapabilities(network) : null; + final boolean connected = (info != null) && info.isConnected(); final boolean validated = (capabilities != null) && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED); - final boolean connected = (info != null) && info.isConnected(); - final boolean connectionUsable = connected && validated; + final boolean sane = isSane(jobStatus, capabilities); + final boolean connectionUsable = connected && validated && sane; final boolean metered = connected && (capabilities != null) && !capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED); diff --git a/services/core/java/com/android/server/job/controllers/JobStatus.java b/services/core/java/com/android/server/job/controllers/JobStatus.java index 1a27c0afb5ea..46ed84e599e8 100644 --- a/services/core/java/com/android/server/job/controllers/JobStatus.java +++ b/services/core/java/com/android/server/job/controllers/JobStatus.java @@ -219,6 +219,8 @@ public final class JobStatus { */ ContentObserverController.JobInstance contentObserverJobInstance; + private long totalNetworkBytes = JobInfo.NETWORK_BYTES_UNKNOWN; + /** Provide a handle to the service that this job will be run on. */ public int getServiceToken() { return callingUid; @@ -296,6 +298,8 @@ public final class JobStatus { mLastSuccessfulRunTime = lastSuccessfulRunTime; mLastFailedRunTime = lastFailedRunTime; + + updateEstimatedNetworkBytesLocked(); } /** Copy constructor: used specifically when cloning JobStatus objects for persistence, @@ -389,6 +393,7 @@ public final class JobStatus { sourcePackageName, sourceUserId, toShortString())); } pendingWork.add(work); + updateEstimatedNetworkBytesLocked(); } public JobWorkItem dequeueWorkLocked() { @@ -401,6 +406,7 @@ public final class JobStatus { executingWork.add(work); work.bumpDeliveryCount(); } + updateEstimatedNetworkBytesLocked(); return work; } return null; @@ -459,6 +465,7 @@ public final class JobStatus { pendingWork = null; executingWork = null; incomingJob.nextPendingWorkId = nextPendingWorkId; + incomingJob.updateEstimatedNetworkBytesLocked(); } else { // We are completely stopping the job... need to clean up work. ungrantWorkList(am, pendingWork); @@ -466,6 +473,7 @@ public final class JobStatus { ungrantWorkList(am, executingWork); executingWork = null; } + updateEstimatedNetworkBytesLocked(); } public void prepareLocked(IActivityManager am) { @@ -568,6 +576,38 @@ public final class JobStatus { return job.getFlags(); } + private void updateEstimatedNetworkBytesLocked() { + totalNetworkBytes = computeEstimatedNetworkBytesLocked(); + } + + private long computeEstimatedNetworkBytesLocked() { + // If any component of the job has unknown usage, we don't have a + // complete picture of what data will be used, and we have to treat the + // entire job as unknown. + long totalNetworkBytes = 0; + long networkBytes = job.getEstimatedNetworkBytes(); + if (networkBytes == JobInfo.NETWORK_BYTES_UNKNOWN) { + return JobInfo.NETWORK_BYTES_UNKNOWN; + } else { + totalNetworkBytes += networkBytes; + } + if (pendingWork != null) { + for (int i = 0; i < pendingWork.size(); i++) { + networkBytes = pendingWork.get(i).getEstimatedNetworkBytes(); + if (networkBytes == JobInfo.NETWORK_BYTES_UNKNOWN) { + return JobInfo.NETWORK_BYTES_UNKNOWN; + } else { + totalNetworkBytes += networkBytes; + } + } + } + return totalNetworkBytes; + } + + public long getEstimatedNetworkBytes() { + return totalNetworkBytes; + } + /** Does this job have any sort of networking constraint? */ public boolean hasConnectivityConstraint() { return (requiredConstraints&CONNECTIVITY_MASK) != 0; @@ -1047,6 +1087,9 @@ public final class JobStatus { if (job.getNetworkType() != JobInfo.NETWORK_TYPE_NONE) { pw.print(prefix); pw.print(" Network type: "); pw.println(job.getNetworkType()); } + if (totalNetworkBytes != JobInfo.NETWORK_BYTES_UNKNOWN) { + pw.print(prefix); pw.print(" Network bytes: "); pw.println(totalNetworkBytes); + } if (job.getMinLatencyMillis() != 0) { pw.print(prefix); pw.print(" Minimum latency: "); TimeUtils.formatDuration(job.getMinLatencyMillis(), pw); |