diff options
author | Justin Klaassen <justinklaassen@google.com> | 2017-10-30 17:25:37 -0400 |
---|---|---|
committer | Justin Klaassen <justinklaassen@google.com> | 2017-10-30 17:25:37 -0400 |
commit | 46c77c203439b3b37c99d09e326df4b1fe08c10b (patch) | |
tree | 70d29abbfbb1106cd0830b33bc7e69e6fb151b1e /android/app | |
parent | 47ed54e5d312f899507d28d6e95ccc18a0de19fe (diff) | |
download | android-28-46c77c203439b3b37c99d09e326df4b1fe08c10b.tar.gz |
Import Android SDK Platform P [4423826]
/google/data/ro/projects/android/fetch_artifact \
--bid 4423826 \
--target sdk_phone_armv7-win_sdk \
sdk-repo-linux-sources-4423826.zip
AndroidVersion.ApiLevel has been modified to appear as 28
Change-Id: I45f7bdc9b9c1cdcba75386623ae5f3ead6db4da8
Diffstat (limited to 'android/app')
26 files changed, 984 insertions, 358 deletions
diff --git a/android/app/Activity.java b/android/app/Activity.java index 85f73bb7..9d331a02 100644 --- a/android/app/Activity.java +++ b/android/app/Activity.java @@ -5867,10 +5867,11 @@ public class Activity extends ContextThemeWrapper } /** - * Returns complete component name of this activity. + * Returns the complete component name of this activity. * * @return Returns the complete component name for this activity */ + @Override public ComponentName getComponentName() { return mComponent; diff --git a/android/app/ActivityManager.java b/android/app/ActivityManager.java index fc4c8d7f..8d9dc1fa 100644 --- a/android/app/ActivityManager.java +++ b/android/app/ActivityManager.java @@ -46,6 +46,7 @@ import android.graphics.Matrix; import android.graphics.Point; import android.graphics.Rect; import android.os.BatteryStats; +import android.os.Binder; import android.os.Build; import android.os.Build.VERSION_CODES; import android.os.Bundle; @@ -500,7 +501,7 @@ public class ActivityManager { public static final int PROCESS_STATE_SERVICE = 11; /** @hide Process is in the background running a receiver. Note that from the - * perspective of oom_adj receivers run at a higher foreground level, but for our + * perspective of oom_adj, receivers run at a higher foreground level, but for our * prioritization here that is not necessary and putting them below services means * many fewer changes in some process states as they receive broadcasts. */ public static final int PROCESS_STATE_RECEIVER = 12; @@ -524,6 +525,20 @@ public class ActivityManager { /** @hide Process does not exist. */ public static final int PROCESS_STATE_NONEXISTENT = 18; + // NOTE: If PROCESS_STATEs are added or changed, then new fields must be added + // to frameworks/base/core/proto/android/app/activitymanager.proto and the following method must + // be updated to correctly map between them. + /** + * Maps ActivityManager.PROCESS_STATE_ values to ActivityManagerProto.ProcessState enum. + * + * @param amInt a process state of the form ActivityManager.PROCESS_STATE_ + * @return the value of the corresponding android.app.ActivityManagerProto's ProcessState enum. + * @hide + */ + public static final int processStateAmToProto(int amInt) { + return amInt * 100; + } + /** @hide The lowest process state number */ public static final int MIN_PROCESS_STATE = PROCESS_STATE_PERSISTENT; @@ -833,9 +848,8 @@ public class ActivityManager { /** * Returns true if this is a low-RAM device. Exactly whether a device is low-RAM * is ultimately up to the device configuration, but currently it generally means - * something in the class of a 512MB device with about a 800x480 or less screen. - * This is mostly intended to be used by apps to determine whether they should turn - * off certain features that require more RAM. + * something with 1GB or less of RAM. This is mostly intended to be used by apps + * to determine whether they should turn off certain features that require more RAM. */ public boolean isLowRamDevice() { return isLowRamDeviceStatic(); @@ -1596,6 +1610,9 @@ public class ActivityManager { public List<RecentTaskInfo> getRecentTasks(int maxNum, int flags) throws SecurityException { try { + if (maxNum < 0) { + throw new IllegalArgumentException("The requested number of tasks should be >= 0"); + } return getService().getRecentTasks(maxNum, flags, UserHandle.myUserId()).getList(); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); @@ -1882,7 +1899,7 @@ public class ActivityManager { public List<RunningTaskInfo> getRunningTasks(int maxNum) throws SecurityException { try { - return getService().getTasks(maxNum, 0); + return getService().getTasks(maxNum); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -3882,21 +3899,36 @@ public class ActivityManager { IBinder service = ServiceManager.checkService(name); if (service == null) { pw.println(" (Service not found)"); + pw.flush(); return; } - TransferPipe tp = null; - try { - pw.flush(); - tp = new TransferPipe(); - tp.setBufferPrefix(" "); - service.dumpAsync(tp.getWriteFd().getFileDescriptor(), args); - tp.go(fd, 10000); - } catch (Throwable e) { - if (tp != null) { - tp.kill(); + pw.flush(); + if (service instanceof Binder) { + // If this is a local object, it doesn't make sense to do an async dump with it, + // just directly dump. + try { + service.dump(fd, args); + } catch (Throwable e) { + pw.println("Failure dumping service:"); + e.printStackTrace(pw); + pw.flush(); + } + } else { + // Otherwise, it is remote, do the dump asynchronously to avoid blocking. + TransferPipe tp = null; + try { + pw.flush(); + tp = new TransferPipe(); + tp.setBufferPrefix(" "); + service.dumpAsync(tp.getWriteFd().getFileDescriptor(), args); + tp.go(fd, 10000); + } catch (Throwable e) { + if (tp != null) { + tp.kill(); + } + pw.println("Failure dumping service:"); + e.printStackTrace(pw); } - pw.println("Failure dumping service:"); - e.printStackTrace(pw); } } diff --git a/android/app/AlarmManager.java b/android/app/AlarmManager.java index 2813e8b9..55f9e289 100644 --- a/android/app/AlarmManager.java +++ b/android/app/AlarmManager.java @@ -33,6 +33,7 @@ import android.os.WorkSource; import android.text.TextUtils; import android.util.ArrayMap; import android.util.Log; +import android.util.proto.ProtoOutputStream; import libcore.util.ZoneInfoDB; @@ -48,7 +49,7 @@ import java.lang.annotation.RetentionPolicy; * if it is not already running. Registered alarms are retained while the * device is asleep (and can optionally wake the device up if they go off * during that time), but will be cleared if it is turned off and rebooted. - * + * * <p>The Alarm Manager holds a CPU wake lock as long as the alarm receiver's * onReceive() method is executing. This guarantees that the phone will not sleep * until you have finished handling the broadcast. Once onReceive() returns, the @@ -296,7 +297,7 @@ public class AlarmManager { * {@link Intent#EXTRA_ALARM_COUNT Intent.EXTRA_ALARM_COUNT} that indicates * how many past alarm events have been accumulated into this intent * broadcast. Recurring alarms that have gone undelivered because the - * phone was asleep may have a count greater than one when delivered. + * phone was asleep may have a count greater than one when delivered. * * <div class="note"> * <p> @@ -396,10 +397,10 @@ public class AlarmManager { * set a recurring alarm for the top of every hour but the phone was asleep * from 7:45 until 8:45, an alarm will be sent as soon as the phone awakens, * then the next alarm will be sent at 9:00. - * - * <p>If your application wants to allow the delivery times to drift in + * + * <p>If your application wants to allow the delivery times to drift in * order to guarantee that at least a certain time interval always elapses - * between alarms, then the approach to take is to use one-time alarms, + * between alarms, then the approach to take is to use one-time alarms, * scheduling the next one yourself when handling each alarm delivery. * * <p class="note"> @@ -1056,7 +1057,7 @@ public class AlarmManager { /** * Creates a new alarm clock description. * - * @param triggerTime time at which the underlying alarm is triggered in wall time + * @param triggerTime time at which the underlying alarm is triggered in wall time * milliseconds since the epoch * @param showIntent an intent that can be used to show or edit details of * the alarm clock. @@ -1089,7 +1090,7 @@ public class AlarmManager { * Returns an intent that can be used to show or edit details of the alarm clock in * the application that scheduled it. * - * <p class="note">Beware that any application can retrieve and send this intent, + * <p class="note">Beware that any application can retrieve and send this intent, * potentially with additional fields filled in. See * {@link PendingIntent#send(android.content.Context, int, android.content.Intent) * PendingIntent.send()} and {@link android.content.Intent#fillIn Intent.fillIn()} @@ -1121,5 +1122,13 @@ public class AlarmManager { return new AlarmClockInfo[size]; } }; + + /** @hide */ + public void writeToProto(ProtoOutputStream proto, long fieldId) { + final long token = proto.start(fieldId); + proto.write(AlarmClockInfoProto.TRIGGER_TIME_MS, mTriggerTime); + mShowIntent.writeToProto(proto, AlarmClockInfoProto.SHOW_INTENT); + proto.end(token); + } } } diff --git a/android/app/Notification.java b/android/app/Notification.java index fee7d6c8..8226e0fb 100644 --- a/android/app/Notification.java +++ b/android/app/Notification.java @@ -858,7 +858,7 @@ public class Notification implements Parcelable * * @hide */ - static public IBinder whitelistToken; + private IBinder mWhitelistToken; /** * Must be set by a process to start associating tokens with Notification objects @@ -1876,12 +1876,12 @@ public class Notification implements Parcelable { int version = parcel.readInt(); - whitelistToken = parcel.readStrongBinder(); - if (whitelistToken == null) { - whitelistToken = processWhitelistToken; + mWhitelistToken = parcel.readStrongBinder(); + if (mWhitelistToken == null) { + mWhitelistToken = processWhitelistToken; } // Propagate this token to all pending intents that are unmarshalled from the parcel. - parcel.setClassCookie(PendingIntent.class, whitelistToken); + parcel.setClassCookie(PendingIntent.class, mWhitelistToken); when = parcel.readLong(); creationTime = parcel.readLong(); @@ -1989,7 +1989,7 @@ public class Notification implements Parcelable * @hide */ public void cloneInto(Notification that, boolean heavy) { - that.whitelistToken = this.whitelistToken; + that.mWhitelistToken = this.mWhitelistToken; that.when = this.when; that.creationTime = this.creationTime; that.mSmallIcon = this.mSmallIcon; @@ -2219,7 +2219,7 @@ public class Notification implements Parcelable private void writeToParcelImpl(Parcel parcel, int flags) { parcel.writeInt(1); - parcel.writeStrongBinder(whitelistToken); + parcel.writeStrongBinder(mWhitelistToken); parcel.writeLong(when); parcel.writeLong(creationTime); if (mSmallIcon == null && icon != 0) { @@ -4981,6 +4981,8 @@ public class Notification implements Parcelable mN.flags |= FLAG_SHOW_LIGHTS; } + mN.allPendingIntents = null; + return mN; } diff --git a/android/app/PendingIntent.java b/android/app/PendingIntent.java index a25c2267..8b76cc7c 100644 --- a/android/app/PendingIntent.java +++ b/android/app/PendingIntent.java @@ -33,6 +33,7 @@ import android.os.Parcelable; import android.os.RemoteException; import android.os.UserHandle; import android.util.AndroidException; +import android.util.proto.ProtoOutputStream; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -1081,7 +1082,16 @@ public final class PendingIntent implements Parcelable { sb.append('}'); return sb.toString(); } - + + /** @hide */ + public void writeToProto(ProtoOutputStream proto, long fieldId) { + final long token = proto.start(fieldId); + if (mTarget != null) { + proto.write(PendingIntentProto.TARGET, mTarget.asBinder().toString()); + } + proto.end(token); + } + public int describeContents() { return 0; } @@ -1119,8 +1129,13 @@ public final class PendingIntent implements Parcelable { */ public static void writePendingIntentOrNullToParcel(@Nullable PendingIntent sender, @NonNull Parcel out) { - out.writeStrongBinder(sender != null ? sender.mTarget.asBinder() - : null); + out.writeStrongBinder(sender != null ? sender.mTarget.asBinder() : null); + if (sender != null) { + OnMarshaledListener listener = sOnMarshaledListener.get(); + if (listener != null) { + listener.onMarshaled(sender, out, 0 /* flags */); + } + } } /** diff --git a/android/app/WindowConfiguration.java b/android/app/WindowConfiguration.java index 251863ca..de27b4fd 100644 --- a/android/app/WindowConfiguration.java +++ b/android/app/WindowConfiguration.java @@ -102,7 +102,7 @@ public class WindowConfiguration implements Parcelable, Comparable<WindowConfigu public static final int ACTIVITY_TYPE_STANDARD = 1; /** Home/Launcher activity type. */ public static final int ACTIVITY_TYPE_HOME = 2; - /** Recents/Overview activity type. */ + /** Recents/Overview activity type. There is only one activity with this type in the system. */ public static final int ACTIVITY_TYPE_RECENTS = 3; /** Assistant activity type. */ public static final int ACTIVITY_TYPE_ASSISTANT = 4; diff --git a/android/app/admin/DevicePolicyManager.java b/android/app/admin/DevicePolicyManager.java index ab8edee7..772c6d60 100644 --- a/android/app/admin/DevicePolicyManager.java +++ b/android/app/admin/DevicePolicyManager.java @@ -1542,6 +1542,92 @@ public class DevicePolicyManager { public @interface ProvisioningPreCondition {} /** + * Disable all configurable SystemUI features during LockTask mode. This includes, + * <ul> + * <li>system info area in the status bar (connectivity icons, clock, etc.) + * <li>notifications (including alerts, icons, and the notification shade) + * <li>Home button + * <li>Recents button and UI + * <li>global actions menu (i.e. power button menu) + * <li>keyguard + * </ul> + * + * This is the default configuration for LockTask. + * + * @see #setLockTaskFeatures(ComponentName, int) + */ + public static final int LOCK_TASK_FEATURE_NONE = 0; + + /** + * Enable the system info area in the status bar during LockTask mode. The system info area + * usually occupies the right side of the status bar (although this can differ across OEMs). It + * includes all system information indicators, such as date and time, connectivity, battery, + * vibration mode, etc. + * + * @see #setLockTaskFeatures(ComponentName, int) + */ + public static final int LOCK_TASK_FEATURE_SYSTEM_INFO = 1; + + /** + * Enable notifications during LockTask mode. This includes notification icons on the status + * bar, heads-up notifications, and the expandable notification shade. Note that the Quick + * Settings panel will still be disabled. + * + * @see #setLockTaskFeatures(ComponentName, int) + */ + public static final int LOCK_TASK_FEATURE_NOTIFICATIONS = 1 << 1; + + /** + * Enable the Home button during LockTask mode. Note that if a custom launcher is used, it has + * to be registered as the default launcher with + * {@link #addPersistentPreferredActivity(ComponentName, IntentFilter, ComponentName)}, and its + * package needs to be whitelisted for LockTask with + * {@link #setLockTaskPackages(ComponentName, String[])}. + * + * @see #setLockTaskFeatures(ComponentName, int) + */ + public static final int LOCK_TASK_FEATURE_HOME = 1 << 2; + + /** + * Enable the Recents button and the Recents screen during LockTask mode. + * + * @see #setLockTaskFeatures(ComponentName, int) + */ + public static final int LOCK_TASK_FEATURE_RECENTS = 1 << 3; + + /** + * Enable the global actions dialog during LockTask mode. This is the dialog that shows up when + * the user long-presses the power button, for example. Note that the user may not be able to + * power off the device if this flag is not set. + * + * @see #setLockTaskFeatures(ComponentName, int) + */ + public static final int LOCK_TASK_FEATURE_GLOBAL_ACTIONS = 1 << 4; + + /** + * Enable the keyguard during LockTask mode. Note that if the keyguard is already disabled with + * {@link #setKeyguardDisabled(ComponentName, boolean)}, setting this flag will have no effect. + * If this flag is not set, the keyguard will not be shown even if the user has a lock screen + * credential. + * + * @see #setLockTaskFeatures(ComponentName, int) + */ + public static final int LOCK_TASK_FEATURE_KEYGUARD = 1 << 5; + + /** + * Flags supplied to {@link #setLockTaskFeatures(ComponentName, int)}. + * + * @hide + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = true, + value = {LOCK_TASK_FEATURE_NONE, LOCK_TASK_FEATURE_SYSTEM_INFO, + LOCK_TASK_FEATURE_NOTIFICATIONS, LOCK_TASK_FEATURE_HOME, + LOCK_TASK_FEATURE_RECENTS, LOCK_TASK_FEATURE_GLOBAL_ACTIONS, + LOCK_TASK_FEATURE_KEYGUARD}) + public @interface LockTaskFeature {} + + /** * Service action: Action for a service that device owner and profile owner can optionally * own. If a device owner or a profile owner has such a service, the system tries to keep * a bound connection to it, in order to keep their process always running. @@ -6484,6 +6570,61 @@ public class DevicePolicyManager { } /** + * Sets which system features to enable for LockTask mode. + * <p> + * Feature flags set through this method will only take effect for the duration when the device + * is in LockTask mode. If this method is not called, none of the features listed here will be + * enabled. + * <p> + * This function can only be called by the device owner or by a profile owner of a user/profile + * that is affiliated with the device owner user. See {@link #setAffiliationIds}. Any features + * set via this method will be cleared if the user becomes unaffiliated. + * + * @param admin Which {@link DeviceAdminReceiver} this request is associated with. + * @param flags Bitfield of feature flags: + * {@link #LOCK_TASK_FEATURE_NONE} (default), + * {@link #LOCK_TASK_FEATURE_SYSTEM_INFO}, + * {@link #LOCK_TASK_FEATURE_NOTIFICATIONS}, + * {@link #LOCK_TASK_FEATURE_HOME}, + * {@link #LOCK_TASK_FEATURE_RECENTS}, + * {@link #LOCK_TASK_FEATURE_GLOBAL_ACTIONS}, + * {@link #LOCK_TASK_FEATURE_KEYGUARD} + * @throws SecurityException if {@code admin} is not the device owner, or the profile owner of + * an affiliated user or profile. + */ + public void setLockTaskFeatures(@NonNull ComponentName admin, @LockTaskFeature int flags) { + throwIfParentInstance("setLockTaskFeatures"); + if (mService != null) { + try { + mService.setLockTaskFeatures(admin, flags); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + } + + /** + * Gets which system features are enabled for LockTask mode. + * + * @param admin Which {@link DeviceAdminReceiver} this request is associated with. + * @return bitfield of flags. See {@link #setLockTaskFeatures(ComponentName, int)} for a list. + * @throws SecurityException if {@code admin} is not the device owner, or the profile owner of + * an affiliated user or profile. + * @see #setLockTaskFeatures(ComponentName, int) + */ + public @LockTaskFeature int getLockTaskFeatures(@NonNull ComponentName admin) { + throwIfParentInstance("getLockTaskFeatures"); + if (mService != null) { + try { + return mService.getLockTaskFeatures(admin); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + return 0; + } + + /** * Called by device owners to update {@link android.provider.Settings.Global} settings. * Validation that the value of the setting is in the correct form for the setting type should * be performed by the caller. @@ -6901,6 +7042,12 @@ public class DevicePolicyManager { * Called by device owner to disable the status bar. Disabling the status bar blocks * notifications, quick settings and other screen overlays that allow escaping from a single use * device. + * <p> + * <strong>Note:</strong> This method has no effect for LockTask mode. The behavior of the + * status bar in LockTask mode can be configured with + * {@link #setLockTaskFeatures(ComponentName, int)}. Calls to this method when the device is in + * LockTask mode will be registered, but will only take effect when the device leaves LockTask + * mode. * * @param admin Which {@link DeviceAdminReceiver} this request is associated with. * @param disabled {@code true} disables the status bar, {@code false} reenables it. diff --git a/android/app/job/JobInfo.java b/android/app/job/JobInfo.java index 1434c9ba..b640bd5b 100644 --- a/android/app/job/JobInfo.java +++ b/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; @@ -909,12 +934,21 @@ public class JobInfo implements Parcelable { } /** - * Set some description of the kind of network type your job needs to have. - * Not calling this function means the network is not necessary, as the default is - * {@link #NETWORK_TYPE_NONE}. - * Bear in mind that calling this function defines network as a strict requirement for your - * job. If the network requested is not available your job will never run. See - * {@link #setOverrideDeadline(long)} to change this behaviour. + * Set some description of the kind of network type your job needs to + * have. Not calling this function means the network is not necessary, + * as the default is {@link #NETWORK_TYPE_NONE}. Bear in mind that + * calling this function defines network as a strict requirement for + * your job. If the network requested is not available your job will + * never run. See {@link #setOverrideDeadline(long)} to change this + * behaviour. + * <p class="note"> + * Note: When your job executes in + * {@link JobService#onStartJob(JobParameters)}, be sure to use the + * specific network returned by {@link JobParameters#getNetwork()}, + * otherwise you'll use the default network which may not meet this + * constraint. + * + * @see JobParameters#getNetwork() */ public Builder setRequiredNetworkType(@NetworkType int networkType) { mNetworkType = networkType; @@ -922,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}. @@ -1147,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/android/app/job/JobParameters.java b/android/app/job/JobParameters.java index a6f6be22..5053dc6f 100644 --- a/android/app/job/JobParameters.java +++ b/android/app/job/JobParameters.java @@ -20,6 +20,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.app.job.IJobCallback; import android.content.ClipData; +import android.net.Network; import android.net.Uri; import android.os.Bundle; import android.os.IBinder; @@ -66,6 +67,7 @@ public class JobParameters implements Parcelable { private final boolean overrideDeadlineExpired; private final Uri[] mTriggeredContentUris; private final String[] mTriggeredContentAuthorities; + private final Network network; private int stopReason; // Default value of stopReason is REASON_CANCELED @@ -73,7 +75,7 @@ public class JobParameters implements Parcelable { public JobParameters(IBinder callback, int jobId, PersistableBundle extras, Bundle transientExtras, ClipData clipData, int clipGrantFlags, boolean overrideDeadlineExpired, Uri[] triggeredContentUris, - String[] triggeredContentAuthorities) { + String[] triggeredContentAuthorities, Network network) { this.jobId = jobId; this.extras = extras; this.transientExtras = transientExtras; @@ -83,6 +85,7 @@ public class JobParameters implements Parcelable { this.overrideDeadlineExpired = overrideDeadlineExpired; this.mTriggeredContentUris = triggeredContentUris; this.mTriggeredContentAuthorities = triggeredContentAuthorities; + this.network = network; } /** @@ -171,6 +174,28 @@ public class JobParameters implements Parcelable { } /** + * Return the network that should be used to perform any network requests + * for this job. + * <p> + * Devices may have multiple active network connections simultaneously, or + * they may not have a default network route at all. To correctly handle all + * situations like this, your job should always use the network returned by + * this method instead of implicitly using the default network route. + * <p> + * Note that the system may relax the constraints you originally requested, + * such as allowing a {@link JobInfo#NETWORK_TYPE_UNMETERED} job to run over + * a metered network when there is a surplus of metered data available. + * + * @return the network that should be used to perform any network requests + * for this job, or {@code null} if this job didn't set any required + * network type. + * @see JobInfo.Builder#setRequiredNetworkType(int) + */ + public @Nullable Network getNetwork() { + return network; + } + + /** * Dequeue the next pending {@link JobWorkItem} from these JobParameters associated with their * currently running job. Calling this method when there is no more work available and all * previously dequeued work has been completed will result in the system taking care of @@ -257,6 +282,11 @@ public class JobParameters implements Parcelable { overrideDeadlineExpired = in.readInt() == 1; mTriggeredContentUris = in.createTypedArray(Uri.CREATOR); mTriggeredContentAuthorities = in.createStringArray(); + if (in.readInt() != 0) { + network = Network.CREATOR.createFromParcel(in); + } else { + network = null; + } stopReason = in.readInt(); } @@ -286,6 +316,12 @@ public class JobParameters implements Parcelable { dest.writeInt(overrideDeadlineExpired ? 1 : 0); dest.writeTypedArray(mTriggeredContentUris, flags); dest.writeStringArray(mTriggeredContentAuthorities); + if (network != null) { + dest.writeInt(1); + network.writeToParcel(dest, flags); + } else { + dest.writeInt(0); + } dest.writeInt(stopReason); } diff --git a/android/app/job/JobWorkItem.java b/android/app/job/JobWorkItem.java index 0eb0450e..1c46e8ec 100644 --- a/android/app/job/JobWorkItem.java +++ b/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/android/app/slice/Slice.java b/android/app/slice/Slice.java index 7f9f74b4..f6b6b869 100644 --- a/android/app/slice/Slice.java +++ b/android/app/slice/Slice.java @@ -154,25 +154,6 @@ public final class Slice implements Parcelable { return Arrays.asList(mHints); } - /** - * @hide - */ - public SliceItem getPrimaryIcon() { - for (SliceItem item : getItems()) { - if (item.getType() == SliceItem.TYPE_IMAGE) { - return item; - } - if (!(item.getType() == SliceItem.TYPE_SLICE && item.hasHint(Slice.HINT_LIST)) - && !item.hasHint(Slice.HINT_ACTIONS) - && !item.hasHint(Slice.HINT_LIST_ITEM) - && (item.getType() != SliceItem.TYPE_ACTION)) { - SliceItem icon = SliceQuery.find(item, SliceItem.TYPE_IMAGE); - if (icon != null) return icon; - } - } - return null; - } - @Override public void writeToParcel(Parcel dest, int flags) { dest.writeStringArray(mHints); @@ -405,6 +386,9 @@ public final class Slice implements Parcelable { final Bundle res = provider.call(resolver.getPackageName(), SliceProvider.METHOD_SLICE, null, extras); Bundle.setDefusable(res, true); + if (res == null) { + return null; + } return res.getParcelable(SliceProvider.EXTRA_SLICE); } catch (RemoteException e) { // Arbitrary and not worth documenting, as Activity diff --git a/android/app/slice/SliceProvider.java b/android/app/slice/SliceProvider.java index df87b455..33825b4b 100644 --- a/android/app/slice/SliceProvider.java +++ b/android/app/slice/SliceProvider.java @@ -156,27 +156,34 @@ public abstract class SliceProvider extends ContentProvider { } private Slice handleBindSlice(Uri sliceUri) { - Slice[] output = new Slice[1]; - CountDownLatch latch = new CountDownLatch(1); - Handler mainHandler = new Handler(Looper.getMainLooper()); - mainHandler.post(() -> { - ThreadPolicy oldPolicy = StrictMode.getThreadPolicy(); - try { - StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() - .detectAll() - .penaltyDeath() - .build()); - output[0] = onBindSlice(sliceUri); - } finally { - StrictMode.setThreadPolicy(oldPolicy); + if (Looper.myLooper() == Looper.getMainLooper()) { + return onBindSliceStrict(sliceUri); + } else { + CountDownLatch latch = new CountDownLatch(1); + Slice[] output = new Slice[1]; + Handler.getMain().post(() -> { + output[0] = onBindSliceStrict(sliceUri); latch.countDown(); + }); + try { + latch.await(); + return output[0]; + } catch (InterruptedException e) { + throw new RuntimeException(e); } - }); + } + } + + private Slice onBindSliceStrict(Uri sliceUri) { + ThreadPolicy oldPolicy = StrictMode.getThreadPolicy(); try { - latch.await(); - return output[0]; - } catch (InterruptedException e) { - throw new RuntimeException(e); + StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() + .detectAll() + .penaltyDeath() + .build()); + return onBindSlice(sliceUri); + } finally { + StrictMode.setThreadPolicy(oldPolicy); } } } diff --git a/android/app/slice/SliceQuery.java b/android/app/slice/SliceQuery.java index d1fe2c90..9943c492 100644 --- a/android/app/slice/SliceQuery.java +++ b/android/app/slice/SliceQuery.java @@ -35,6 +35,27 @@ public class SliceQuery { /** * @hide */ + public static SliceItem getPrimaryIcon(Slice slice) { + for (SliceItem item : slice.getItems()) { + if (item.getType() == SliceItem.TYPE_IMAGE) { + return item; + } + if (!(item.getType() == SliceItem.TYPE_SLICE && item.hasHint(Slice.HINT_LIST)) + && !item.hasHint(Slice.HINT_ACTIONS) + && !item.hasHint(Slice.HINT_LIST_ITEM) + && (item.getType() != SliceItem.TYPE_ACTION)) { + SliceItem icon = SliceQuery.find(item, SliceItem.TYPE_IMAGE); + if (icon != null) { + return icon; + } + } + } + return null; + } + + /** + * @hide + */ public static SliceItem findNotContaining(SliceItem container, List<SliceItem> list) { SliceItem ret = null; while (ret == null && list.size() != 0) { diff --git a/android/app/slice/views/SliceView.java b/android/app/slice/views/SliceView.java deleted file mode 100644 index 32484fca..00000000 --- a/android/app/slice/views/SliceView.java +++ /dev/null @@ -1,251 +0,0 @@ -/* - * Copyright (C) 2017 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.app.slice.views; - -import android.annotation.StringDef; -import android.app.slice.Slice; -import android.app.slice.SliceItem; -import android.app.slice.SliceQuery; -import android.content.ContentResolver; -import android.content.Context; -import android.content.Intent; -import android.graphics.drawable.ColorDrawable; -import android.net.Uri; -import android.util.Log; -import android.view.MotionEvent; -import android.view.View; -import android.widget.FrameLayout; -import android.widget.LinearLayout; - -import java.util.List; - -/** - * A view that can display a {@link Slice} in different {@link SliceMode}'s. - * - * @hide - */ -public class SliceView extends LinearLayout { - - private static final String TAG = "SliceView"; - - /** - * @hide - */ - public abstract static class SliceModeView extends FrameLayout { - - public SliceModeView(Context context) { - super(context); - } - - /** - * @return the {@link SliceMode} of the slice being presented. - */ - public abstract String getMode(); - - /** - * @param slice the slice to show in this view. - */ - public abstract void setSlice(Slice slice); - } - - /** - * @hide - */ - @StringDef({ - MODE_SMALL, MODE_LARGE, MODE_SHORTCUT - }) - public @interface SliceMode {} - - /** - * Mode indicating this slice should be presented in small template format. - */ - public static final String MODE_SMALL = "SLICE_SMALL"; - /** - * Mode indicating this slice should be presented in large template format. - */ - public static final String MODE_LARGE = "SLICE_LARGE"; - /** - * Mode indicating this slice should be presented as an icon. - */ - public static final String MODE_SHORTCUT = "SLICE_ICON"; - - /** - * Will select the type of slice binding based on size of the View. TODO: Put in some info about - * that selection. - */ - private static final String MODE_AUTO = "auto"; - - private String mMode = MODE_AUTO; - private SliceModeView mCurrentView; - private final ActionRow mActions; - private Slice mCurrentSlice; - private boolean mShowActions = true; - - /** - * Simple constructor to create a slice view from code. - * - * @param context The context the view is running in. - */ - public SliceView(Context context) { - super(context); - setOrientation(LinearLayout.VERTICAL); - mActions = new ActionRow(mContext, true); - mActions.setBackground(new ColorDrawable(0xffeeeeee)); - mCurrentView = new LargeTemplateView(mContext); - addView(mCurrentView); - addView(mActions); - } - - /** - * @hide - */ - public void bindSlice(Intent intent) { - // TODO - } - - /** - * Binds this view to the {@link Slice} associated with the provided {@link Uri}. - */ - public void bindSlice(Uri sliceUri) { - validate(sliceUri); - Slice s = Slice.bindSlice(mContext.getContentResolver(), sliceUri); - bindSlice(s); - } - - /** - * Binds this view to the provided {@link Slice}. - */ - public void bindSlice(Slice slice) { - mCurrentSlice = slice; - if (mCurrentSlice != null) { - reinflate(); - } - } - - /** - * Call to clean up the view. - */ - public void unbindSlice() { - mCurrentSlice = null; - } - - /** - * Set the {@link SliceMode} this view should present in. - */ - public void setMode(@SliceMode String mode) { - setMode(mode, false /* animate */); - } - - /** - * @hide - */ - public void setMode(@SliceMode String mode, boolean animate) { - if (animate) { - Log.e(TAG, "Animation not supported yet"); - } - mMode = mode; - reinflate(); - } - - /** - * @return the {@link SliceMode} this view is presenting in. - */ - public @SliceMode String getMode() { - if (mMode.equals(MODE_AUTO)) { - return MODE_LARGE; - } - return mMode; - } - - /** - * @hide - * - * Whether this view should show a row of actions with it. - */ - public void setShowActionRow(boolean show) { - mShowActions = show; - reinflate(); - } - - private SliceModeView createView(String mode) { - switch (mode) { - case MODE_SHORTCUT: - return new ShortcutView(getContext()); - case MODE_SMALL: - return new SmallTemplateView(getContext()); - } - return new LargeTemplateView(getContext()); - } - - @Override - protected void onDetachedFromWindow() { - super.onDetachedFromWindow(); - unbindSlice(); - } - - private void reinflate() { - if (mCurrentSlice == null) { - return; - } - // TODO: Smarter mapping here from one state to the next. - SliceItem color = SliceQuery.find(mCurrentSlice, SliceItem.TYPE_COLOR); - List<SliceItem> items = mCurrentSlice.getItems(); - SliceItem actionRow = SliceQuery.find(mCurrentSlice, SliceItem.TYPE_SLICE, - Slice.HINT_ACTIONS, - Slice.HINT_ALT); - String mode = getMode(); - if (!mode.equals(mCurrentView.getMode())) { - removeAllViews(); - mCurrentView = createView(mode); - addView(mCurrentView); - addView(mActions); - } - if (items.size() > 1 || (items.size() != 0 && items.get(0) != actionRow)) { - mCurrentView.setVisibility(View.VISIBLE); - mCurrentView.setSlice(mCurrentSlice); - } else { - mCurrentView.setVisibility(View.GONE); - } - - boolean showActions = mShowActions && actionRow != null - && !mode.equals(MODE_SHORTCUT); - if (showActions) { - mActions.setActions(actionRow, color); - mActions.setVisibility(View.VISIBLE); - } else { - mActions.setVisibility(View.GONE); - } - } - - @Override - public boolean onInterceptTouchEvent(MotionEvent ev) { - // TODO -- may need to rethink for AGSA - if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) { - requestDisallowInterceptTouchEvent(true); - } - return super.onInterceptTouchEvent(ev); - } - - private static void validate(Uri sliceUri) { - if (!ContentResolver.SCHEME_CONTENT.equals(sliceUri.getScheme())) { - throw new RuntimeException("Invalid uri " + sliceUri); - } - if (sliceUri.getPathSegments().size() == 0) { - throw new RuntimeException("Invalid uri " + sliceUri); - } - } -} diff --git a/android/app/slice/views/ActionRow.java b/android/app/slice/widget/ActionRow.java index c7d99f7f..c96e6304 100644 --- a/android/app/slice/views/ActionRow.java +++ b/android/app/slice/widget/ActionRow.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package android.app.slice.views; +package android.app.slice.widget; import android.app.PendingIntent; import android.app.PendingIntent.CanceledException; diff --git a/android/app/slice/views/GridView.java b/android/app/slice/widget/GridView.java index 6f30c507..67a3c671 100644 --- a/android/app/slice/views/GridView.java +++ b/android/app/slice/widget/GridView.java @@ -14,14 +14,14 @@ * limitations under the License. */ -package android.app.slice.views; +package android.app.slice.widget; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; import android.app.slice.Slice; import android.app.slice.SliceItem; -import android.app.slice.views.LargeSliceAdapter.SliceListView; +import android.app.slice.widget.LargeSliceAdapter.SliceListView; import android.content.Context; import android.graphics.Color; import android.util.AttributeSet; diff --git a/android/app/slice/views/LargeSliceAdapter.java b/android/app/slice/widget/LargeSliceAdapter.java index 6794ff98..267fff6a 100644 --- a/android/app/slice/views/LargeSliceAdapter.java +++ b/android/app/slice/widget/LargeSliceAdapter.java @@ -14,12 +14,12 @@ * limitations under the License. */ -package android.app.slice.views; +package android.app.slice.widget; import android.app.slice.Slice; import android.app.slice.SliceItem; import android.app.slice.SliceQuery; -import android.app.slice.views.LargeSliceAdapter.SliceViewHolder; +import android.app.slice.widget.LargeSliceAdapter.SliceViewHolder; import android.content.Context; import android.util.ArrayMap; import android.view.LayoutInflater; @@ -71,7 +71,7 @@ public class LargeSliceAdapter extends RecyclerView.Adapter<SliceViewHolder> { @Override public SliceViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - View v = inflateforType(viewType); + View v = inflateForType(viewType); v.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); return new SliceViewHolder(v); } @@ -104,7 +104,7 @@ public class LargeSliceAdapter extends RecyclerView.Adapter<SliceViewHolder> { } } - private View inflateforType(int viewType) { + private View inflateForType(int viewType) { switch (viewType) { case TYPE_REMOTE_VIEWS: return new FrameLayout(mContext); diff --git a/android/app/slice/views/LargeTemplateView.java b/android/app/slice/widget/LargeTemplateView.java index 9e225162..f45b2a8f 100644 --- a/android/app/slice/views/LargeTemplateView.java +++ b/android/app/slice/widget/LargeTemplateView.java @@ -14,14 +14,14 @@ * limitations under the License. */ -package android.app.slice.views; +package android.app.slice.widget; import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; import android.app.slice.Slice; import android.app.slice.SliceItem; import android.app.slice.SliceQuery; -import android.app.slice.views.SliceView.SliceModeView; +import android.app.slice.widget.SliceView.SliceModeView; import android.content.Context; import android.util.TypedValue; @@ -35,11 +35,13 @@ import java.util.List; * @hide */ public class LargeTemplateView extends SliceModeView { + private final LargeSliceAdapter mAdapter; private final RecyclerView mRecyclerView; private final int mDefaultHeight; private final int mMaxHeight; private Slice mSlice; + private boolean mIsScrollable; public LargeTemplateView(Context context) { super(context); @@ -49,9 +51,6 @@ public class LargeTemplateView extends SliceModeView { mAdapter = new LargeSliceAdapter(context); mRecyclerView.setAdapter(mAdapter); addView(mRecyclerView); - int width = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 300, - getResources().getDisplayMetrics()); - setLayoutParams(new LayoutParams(width, WRAP_CONTENT)); mDefaultHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 200, getResources().getDisplayMetrics()); mMaxHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 200, @@ -68,7 +67,7 @@ public class LargeTemplateView extends SliceModeView { mRecyclerView.getLayoutParams().height = WRAP_CONTENT; super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (mRecyclerView.getMeasuredHeight() > mMaxHeight - || mSlice.hasHint(Slice.HINT_PARTIAL)) { + || (mSlice != null && mSlice.hasHint(Slice.HINT_PARTIAL))) { mRecyclerView.getLayoutParams().height = mDefaultHeight; } else { mRecyclerView.getLayoutParams().height = mRecyclerView.getMeasuredHeight(); @@ -112,4 +111,12 @@ public class LargeTemplateView extends SliceModeView { sliceItems.forEach(i -> i.addHint(Slice.HINT_LIST_ITEM)); items.addAll(sliceItems); } + + /** + * Whether or not the content in this template should be scrollable. + */ + public void setScrollable(boolean isScrollable) { + // TODO -- restrict / enable how much this view can show + mIsScrollable = isScrollable; + } } diff --git a/android/app/slice/views/MessageView.java b/android/app/slice/widget/MessageView.java index 77252bf2..3124398e 100644 --- a/android/app/slice/views/MessageView.java +++ b/android/app/slice/widget/MessageView.java @@ -14,12 +14,12 @@ * limitations under the License. */ -package android.app.slice.views; +package android.app.slice.widget; import android.app.slice.Slice; import android.app.slice.SliceItem; import android.app.slice.SliceQuery; -import android.app.slice.views.LargeSliceAdapter.SliceListView; +import android.app.slice.widget.LargeSliceAdapter.SliceListView; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; diff --git a/android/app/slice/views/RemoteInputView.java b/android/app/slice/widget/RemoteInputView.java index e53cb1ea..6eff5afb 100644 --- a/android/app/slice/views/RemoteInputView.java +++ b/android/app/slice/widget/RemoteInputView.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package android.app.slice.views; +package android.app.slice.widget; import android.animation.Animator; import android.app.Notification; diff --git a/android/app/slice/views/ShortcutView.java b/android/app/slice/widget/ShortcutView.java index b6790c7d..0bca8ce2 100644 --- a/android/app/slice/views/ShortcutView.java +++ b/android/app/slice/widget/ShortcutView.java @@ -14,21 +14,20 @@ * limitations under the License. */ -package android.app.slice.views; +package android.app.slice.widget; import android.app.PendingIntent; import android.app.PendingIntent.CanceledException; import android.app.slice.Slice; import android.app.slice.SliceItem; import android.app.slice.SliceQuery; -import android.app.slice.views.SliceView.SliceModeView; +import android.app.slice.widget.SliceView.SliceModeView; import android.content.Context; import android.content.Intent; import android.graphics.Color; import android.graphics.drawable.ShapeDrawable; import android.graphics.drawable.shapes.OvalShape; import android.net.Uri; -import android.view.ViewGroup; import com.android.internal.R; @@ -46,17 +45,14 @@ public class ShortcutView extends SliceModeView { public ShortcutView(Context context) { super(context); - mLargeIconSize = getContext().getResources() - .getDimensionPixelSize(R.dimen.slice_shortcut_size); mSmallIconSize = getContext().getResources().getDimensionPixelSize(R.dimen.slice_icon_size); - setLayoutParams(new ViewGroup.LayoutParams(mLargeIconSize, mLargeIconSize)); } @Override public void setSlice(Slice slice) { removeAllViews(); SliceItem sliceItem = SliceQuery.find(slice, SliceItem.TYPE_ACTION); - SliceItem iconItem = slice.getPrimaryIcon(); + SliceItem iconItem = SliceQuery.getPrimaryIcon(slice); SliceItem textItem = sliceItem != null ? SliceQuery.find(sliceItem, SliceItem.TYPE_TEXT) : SliceQuery.find(slice, SliceItem.TYPE_TEXT); diff --git a/android/app/slice/widget/SliceView.java b/android/app/slice/widget/SliceView.java new file mode 100644 index 00000000..5bafbc03 --- /dev/null +++ b/android/app/slice/widget/SliceView.java @@ -0,0 +1,402 @@ +/* + * Copyright (C) 2017 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.app.slice.widget; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.StringDef; +import android.app.slice.Slice; +import android.app.slice.SliceItem; +import android.app.slice.SliceQuery; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.database.ContentObserver; +import android.graphics.drawable.ColorDrawable; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +import com.android.internal.R; +import com.android.internal.util.Preconditions; + +import java.util.List; + +/** + * A view for displaying a {@link Slice} which is a piece of app content and actions. SliceView is + * able to present slice content in a templated format outside of the associated app. The way this + * content is displayed depends on the structure of the slice, the hints associated with the + * content, and the mode that SliceView is configured for. The modes that SliceView supports are: + * <ul> + * <li><b>Shortcut</b>: A shortcut is presented as an icon and a text label representing the main + * content or action associated with the slice.</li> + * <li><b>Small</b>: The small format has a restricted height and can present a single + * {@link SliceItem} or a limited collection of items.</li> + * <li><b>Large</b>: The large format displays multiple small templates in a list, if scrolling is + * not enabled (see {@link #setScrollable(boolean)}) the view will show as many items as it can + * comfortably fit.</li> + * </ul> + * <p> + * When constructing a slice, the contents of it can be annotated with hints, these provide the OS + * with some information on how the content should be displayed. For example, text annotated with + * {@link Slice#HINT_TITLE} would be placed in the title position of a template. A slice annotated + * with {@link Slice#HINT_LIST} would present the child items of that slice in a list. + * <p> + * SliceView can be provided a slice via a uri {@link #setSlice(Uri)} in which case a content + * observer will be set for that uri and the view will update if there are any changes to the slice. + * To use this the app must have a special permission to bind to the slice (see + * {@link android.Manifest.permission#BIND_SLICE}). + * <p> + * Example usage: + * + * <pre class="prettyprint"> + * SliceView v = new SliceView(getContext()); + * v.setMode(desiredMode); + * v.setSlice(sliceUri); + * </pre> + */ +public class SliceView extends ViewGroup { + + private static final String TAG = "SliceView"; + + /** + * @hide + */ + public abstract static class SliceModeView extends FrameLayout { + + public SliceModeView(Context context) { + super(context); + } + + /** + * @return the mode of the slice being presented. + */ + public abstract String getMode(); + + /** + * @param slice the slice to show in this view. + */ + public abstract void setSlice(Slice slice); + } + + /** + * @hide + */ + @StringDef({ + MODE_SMALL, MODE_LARGE, MODE_SHORTCUT + }) + public @interface SliceMode {} + + /** + * Mode indicating this slice should be presented in small template format. + */ + public static final String MODE_SMALL = "SLICE_SMALL"; + /** + * Mode indicating this slice should be presented in large template format. + */ + public static final String MODE_LARGE = "SLICE_LARGE"; + /** + * Mode indicating this slice should be presented as an icon. + */ + public static final String MODE_SHORTCUT = "SLICE_ICON"; + + /** + * Will select the type of slice binding based on size of the View. TODO: Put in some info about + * that selection. + */ + private static final String MODE_AUTO = "auto"; + + private String mMode = MODE_AUTO; + private SliceModeView mCurrentView; + private final ActionRow mActions; + private Slice mCurrentSlice; + private boolean mShowActions = true; + private boolean mIsScrollable; + private SliceObserver mObserver; + private final int mShortcutSize; + + public SliceView(Context context) { + this(context, null); + } + + public SliceView(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public SliceView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public SliceView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + mObserver = new SliceObserver(new Handler(Looper.getMainLooper())); + mActions = new ActionRow(mContext, true); + mActions.setBackground(new ColorDrawable(0xffeeeeee)); + mCurrentView = new LargeTemplateView(mContext); + addView(mCurrentView, getChildLp(mCurrentView)); + addView(mActions, getChildLp(mActions)); + mShortcutSize = getContext().getResources() + .getDimensionPixelSize(R.dimen.slice_shortcut_size); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + measureChildren(widthMeasureSpec, heightMeasureSpec); + int actionHeight = mActions.getVisibility() != View.GONE + ? mActions.getMeasuredHeight() + : 0; + int newHeightSpec = MeasureSpec.makeMeasureSpec( + mCurrentView.getMeasuredHeight() + actionHeight, MeasureSpec.EXACTLY); + int width = MeasureSpec.getSize(widthMeasureSpec); + setMeasuredDimension(width, newHeightSpec); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + mCurrentView.layout(l, t, l + mCurrentView.getMeasuredWidth(), + t + mCurrentView.getMeasuredHeight()); + if (mActions.getVisibility() != View.GONE) { + mActions.layout(l, mCurrentView.getMeasuredHeight(), l + mActions.getMeasuredWidth(), + mCurrentView.getMeasuredHeight() + mActions.getMeasuredHeight()); + } + } + + /** + * @hide + */ + public void showSlice(Intent intent) { + // TODO + } + + /** + * Populates this view with the {@link Slice} associated with the provided {@link Uri}. To use + * this method your app must have the permission + * {@link android.Manifest.permission#BIND_SLICE}). + * <p> + * Setting a slice differs from {@link #showSlice(Slice)} because it will ensure the view is + * updated when the slice identified by the provided URI changes. The lifecycle of this observer + * is handled by SliceView in {@link #onAttachedToWindow()} and {@link #onDetachedFromWindow()}. + * To unregister this observer outside of that you can call {@link #clearSlice}. + * + * @return true if the a slice was found for the provided uri. + * @see #clearSlice + */ + public boolean setSlice(@NonNull Uri sliceUri) { + Preconditions.checkNotNull(sliceUri, + "Uri cannot be null, to remove the slice use clearSlice()"); + if (sliceUri == null) { + clearSlice(); + return false; + } + validate(sliceUri); + Slice s = Slice.bindSlice(mContext.getContentResolver(), sliceUri); + if (s != null) { + mObserver = new SliceObserver(new Handler(Looper.getMainLooper())); + if (isAttachedToWindow()) { + registerSlice(sliceUri); + } + showSlice(s); + } + return s != null; + } + + /** + * Populates this view to the provided {@link Slice}. + * <p> + * This does not register a content observer on the URI that the slice is backed by so it will + * not update if the content changes. To have the view update when the content changes use + * {@link #setSlice(Uri)} instead. Unlike {@link #setSlice(Uri)}, this method does not require + * any special permissions. + */ + public void showSlice(@NonNull Slice slice) { + Preconditions.checkNotNull(slice, + "Slice cannot be null, to remove the slice use clearSlice()"); + clearSlice(); + mCurrentSlice = slice; + reinflate(); + } + + /** + * Unregisters the change observer that is set when using {@link #setSlice}. Normally this is + * done automatically during {@link #onDetachedFromWindow()}. + * <p> + * It is safe to call this method multiple times. + */ + public void clearSlice() { + mCurrentSlice = null; + if (mObserver != null) { + getContext().getContentResolver().unregisterContentObserver(mObserver); + mObserver = null; + } + } + + /** + * Set the mode this view should present in. + */ + public void setMode(@SliceMode String mode) { + setMode(mode, false /* animate */); + } + + /** + * Set whether this view should allow scrollable content when presenting in {@link #MODE_LARGE}. + */ + public void setScrollable(boolean isScrollable) { + mIsScrollable = isScrollable; + reinflate(); + } + + /** + * @hide + */ + public void setMode(@SliceMode String mode, boolean animate) { + if (animate) { + Log.e(TAG, "Animation not supported yet"); + } + mMode = mode; + reinflate(); + } + + /** + * @return the mode this view is presenting in. + */ + public @SliceMode String getMode() { + if (mMode.equals(MODE_AUTO)) { + return MODE_LARGE; + } + return mMode; + } + + /** + * @hide + * + * Whether this view should show a row of actions with it. + */ + public void setShowActionRow(boolean show) { + mShowActions = show; + reinflate(); + } + + private SliceModeView createView(String mode) { + switch (mode) { + case MODE_SHORTCUT: + return new ShortcutView(getContext()); + case MODE_SMALL: + return new SmallTemplateView(getContext()); + } + return new LargeTemplateView(getContext()); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + registerSlice(mCurrentSlice != null ? mCurrentSlice.getUri() : null); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + if (mObserver != null) { + getContext().getContentResolver().unregisterContentObserver(mObserver); + mObserver = null; + } + } + + private void registerSlice(Uri sliceUri) { + if (sliceUri == null || mObserver == null) { + return; + } + mContext.getContentResolver().registerContentObserver(sliceUri, + false /* notifyForDescendants */, mObserver); + } + + private void reinflate() { + if (mCurrentSlice == null) { + return; + } + // TODO: Smarter mapping here from one state to the next. + SliceItem color = SliceQuery.find(mCurrentSlice, SliceItem.TYPE_COLOR); + List<SliceItem> items = mCurrentSlice.getItems(); + SliceItem actionRow = SliceQuery.find(mCurrentSlice, SliceItem.TYPE_SLICE, + Slice.HINT_ACTIONS, + Slice.HINT_ALT); + String mode = getMode(); + if (!mode.equals(mCurrentView.getMode())) { + removeAllViews(); + mCurrentView = createView(mode); + addView(mCurrentView, getChildLp(mCurrentView)); + addView(mActions, getChildLp(mActions)); + } + if (mode.equals(MODE_LARGE)) { + ((LargeTemplateView) mCurrentView).setScrollable(mIsScrollable); + } + if (items.size() > 1 || (items.size() != 0 && items.get(0) != actionRow)) { + mCurrentView.setVisibility(View.VISIBLE); + mCurrentView.setSlice(mCurrentSlice); + } else { + mCurrentView.setVisibility(View.GONE); + } + + boolean showActions = mShowActions && actionRow != null + && !mode.equals(MODE_SHORTCUT); + if (showActions) { + mActions.setActions(actionRow, color); + mActions.setVisibility(View.VISIBLE); + } else { + mActions.setVisibility(View.GONE); + } + } + + private LayoutParams getChildLp(View child) { + if (child instanceof ShortcutView) { + return new LayoutParams(mShortcutSize, mShortcutSize); + } else { + return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); + } + } + + private static void validate(Uri sliceUri) { + if (!ContentResolver.SCHEME_CONTENT.equals(sliceUri.getScheme())) { + throw new RuntimeException("Invalid uri " + sliceUri); + } + if (sliceUri.getPathSegments().size() == 0) { + throw new RuntimeException("Invalid uri " + sliceUri); + } + } + + private class SliceObserver extends ContentObserver { + SliceObserver(Handler handler) { + super(handler); + } + + @Override + public void onChange(boolean selfChange) { + this.onChange(selfChange, null); + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + Slice s = Slice.bindSlice(mContext.getContentResolver(), uri); + mCurrentSlice = s; + reinflate(); + } + } +} diff --git a/android/app/slice/views/SliceViewUtil.java b/android/app/slice/widget/SliceViewUtil.java index 19e8e7c9..03669983 100644 --- a/android/app/slice/views/SliceViewUtil.java +++ b/android/app/slice/widget/SliceViewUtil.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package android.app.slice.views; +package android.app.slice.widget; import android.annotation.ColorInt; import android.content.Context; diff --git a/android/app/slice/views/SmallTemplateView.java b/android/app/slice/widget/SmallTemplateView.java index 42b2d213..1c4c5df2 100644 --- a/android/app/slice/views/SmallTemplateView.java +++ b/android/app/slice/widget/SmallTemplateView.java @@ -14,14 +14,14 @@ * limitations under the License. */ -package android.app.slice.views; +package android.app.slice.widget; import android.app.PendingIntent.CanceledException; import android.app.slice.Slice; import android.app.slice.SliceItem; import android.app.slice.SliceQuery; -import android.app.slice.views.LargeSliceAdapter.SliceListView; -import android.app.slice.views.SliceView.SliceModeView; +import android.app.slice.widget.LargeSliceAdapter.SliceListView; +import android.app.slice.widget.SliceView.SliceModeView; import android.content.Context; import android.os.AsyncTask; import android.view.View; diff --git a/android/app/usage/AppStandby.java b/android/app/usage/AppStandby.java new file mode 100644 index 00000000..6f9fc2fa --- /dev/null +++ b/android/app/usage/AppStandby.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2017 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.app.usage; + +import android.annotation.IntDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Set of constants for app standby buckets and reasons. Apps will be moved into different buckets + * that affect how frequently they can run in the background or perform other battery-consuming + * actions. Buckets will be assigned based on how frequently or when the system thinks the user + * is likely to use the app. + * @hide + */ +public class AppStandby { + + /** The app was used very recently, currently in use or likely to be used very soon. */ + public static final int STANDBY_BUCKET_ACTIVE = 0; + + // Leave some gap in case we want to increase the number of buckets + + /** The app was used recently and/or likely to be used in the next few hours */ + public static final int STANDBY_BUCKET_WORKING_SET = 3; + + // Leave some gap in case we want to increase the number of buckets + + /** The app was used in the last few days and/or likely to be used in the next few days */ + public static final int STANDBY_BUCKET_FREQUENT = 6; + + // Leave some gap in case we want to increase the number of buckets + + /** The app has not be used for several days and/or is unlikely to be used for several days */ + public static final int STANDBY_BUCKET_RARE = 9; + + // Leave some gap in case we want to increase the number of buckets + + /** The app has never been used. */ + public static final int STANDBY_BUCKET_NEVER = 12; + + /** Reason for bucketing -- default initial state */ + public static final String REASON_DEFAULT = "default"; + + /** Reason for bucketing -- timeout */ + public static final String REASON_TIMEOUT = "timeout"; + + /** Reason for bucketing -- usage */ + public static final String REASON_USAGE = "usage"; + + /** Reason for bucketing -- forced by user / shell command */ + public static final String REASON_FORCED = "forced"; + + /** + * Reason for bucketing -- predicted. This is a prefix and the UID of the bucketeer will + * be appended. + */ + public static final String REASON_PREDICTED = "predicted"; + + @IntDef(flag = false, value = { + STANDBY_BUCKET_ACTIVE, + STANDBY_BUCKET_WORKING_SET, + STANDBY_BUCKET_FREQUENT, + STANDBY_BUCKET_RARE, + STANDBY_BUCKET_NEVER, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface StandbyBuckets {} +} diff --git a/android/app/usage/UsageStatsManager.java b/android/app/usage/UsageStatsManager.java index fd579fce..c827432a 100644 --- a/android/app/usage/UsageStatsManager.java +++ b/android/app/usage/UsageStatsManager.java @@ -19,6 +19,7 @@ package android.app.usage; import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.annotation.SystemService; +import android.app.usage.AppStandby.StandbyBuckets; import android.content.Context; import android.content.pm.ParceledListSlice; import android.os.RemoteException; @@ -247,6 +248,29 @@ public final class UsageStatsManager { } /** + * @hide + */ + public @StandbyBuckets int getAppStandbyBucket(String packageName) { + try { + return mService.getAppStandbyBucket(packageName, mContext.getOpPackageName(), + mContext.getUserId()); + } catch (RemoteException e) { + } + return AppStandby.STANDBY_BUCKET_ACTIVE; + } + + /** + * @hide + */ + public void setAppStandbyBucket(String packageName, @StandbyBuckets int bucket) { + try { + mService.setAppStandbyBucket(packageName, bucket, mContext.getUserId()); + } catch (RemoteException e) { + // Nothing to do + } + } + + /** * {@hide} * Temporarily whitelist the specified app for a short duration. This is to allow an app * receiving a high priority message to be able to access the network and acquire wakelocks |