diff options
author | Justin Klaassen <justinklaassen@google.com> | 2017-10-24 19:50:40 -0400 |
---|---|---|
committer | Justin Klaassen <justinklaassen@google.com> | 2017-10-24 19:50:40 -0400 |
commit | 47ed54e5d312f899507d28d6e95ccc18a0de19fe (patch) | |
tree | 7a2d435c55c36fbc1d07e895bd0c68b18f84e12c /android/app | |
parent | 07f9f65561c2b81bcd189b895b31bb2ad0438d74 (diff) | |
download | android-28-47ed54e5d312f899507d28d6e95ccc18a0de19fe.tar.gz |
Import Android SDK Platform P [4413397]
/google/data/ro/projects/android/fetch_artifact \
--bid 4413397 \
--target sdk_phone_armv7-win_sdk \
sdk-repo-linux-sources-4413397.zip
AndroidVersion.ApiLevel has been modified to appear as 28
Change-Id: I3cf1f7c36e61c090dcc7de7bcfa812ef2bf96c00
Diffstat (limited to 'android/app')
29 files changed, 3455 insertions, 311 deletions
diff --git a/android/app/Activity.java b/android/app/Activity.java index e0ac9113..85f73bb7 100644 --- a/android/app/Activity.java +++ b/android/app/Activity.java @@ -542,9 +542,9 @@ import java.util.List; * <ul> * <li> <p>When creating a new document, the backing database entry or file for * it is created immediately. For example, if the user chooses to write - * a new e-mail, a new entry for that e-mail is created as soon as they + * a new email, a new entry for that email is created as soon as they * start entering data, so that if they go to any other activity after - * that point this e-mail will now appear in the list of drafts.</p> + * that point this email will now appear in the list of drafts.</p> * <li> <p>When an activity's <code>onPause()</code> method is called, it should * commit to the backing content provider or file any changes the user * has made. This ensures that those changes will be seen by any other @@ -1879,7 +1879,7 @@ public class Activity extends ContextThemeWrapper if (isFinishing()) { if (mAutoFillResetNeeded) { - getAutofillManager().commit(); + getAutofillManager().onActivityFinished(); } else if (mIntent != null && mIntent.hasExtra(AutofillManager.EXTRA_RESTORE_SESSION_TOKEN)) { // Activity was launched when user tapped a link in the Autofill Save UI - since @@ -6259,6 +6259,8 @@ public class Activity extends ContextThemeWrapper final AutofillManager afm = getAutofillManager(); if (afm != null) { afm.dump(prefix, writer); + } else { + writer.print(prefix); writer.println("No AutofillManager"); } } diff --git a/android/app/ActivityManager.java b/android/app/ActivityManager.java index 5e61727f..fc4c8d7f 100644 --- a/android/app/ActivityManager.java +++ b/android/app/ActivityManager.java @@ -16,14 +16,8 @@ package android.app; -import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; -import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; -import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; -import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY; -import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY; -import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; - import android.Manifest; +import android.annotation.DrawableRes; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -670,138 +664,6 @@ public class ActivityManager { /** Invalid stack ID. */ public static final int INVALID_STACK_ID = -1; - /** First static stack ID. - * @hide */ - private static final int FIRST_STATIC_STACK_ID = 0; - - /** ID of stack where fullscreen activities are normally launched into. - * @hide */ - public static final int FULLSCREEN_WORKSPACE_STACK_ID = 1; - - /** ID of stack where freeform/resized activities are normally launched into. - * @hide */ - public static final int FREEFORM_WORKSPACE_STACK_ID = FULLSCREEN_WORKSPACE_STACK_ID + 1; - - /** ID of stack that occupies a dedicated region of the screen. - * @hide */ - public static final int DOCKED_STACK_ID = FREEFORM_WORKSPACE_STACK_ID + 1; - - /** ID of stack that always on top (always visible) when it exist. - * @hide */ - public static final int PINNED_STACK_ID = DOCKED_STACK_ID + 1; - - /** Last static stack stack ID. - * @hide */ - private static final int LAST_STATIC_STACK_ID = PINNED_STACK_ID; - - /** Start of ID range used by stacks that are created dynamically. - * @hide */ - public static final int FIRST_DYNAMIC_STACK_ID = LAST_STATIC_STACK_ID + 1; - - // TODO: Figure-out a way to remove this. - /** @hide */ - public static boolean isStaticStack(int stackId) { - return stackId >= FIRST_STATIC_STACK_ID && stackId <= LAST_STATIC_STACK_ID; - } - - // TODO: It seems this mostly means a stack on a secondary display now. Need to see if - // there are other meanings. If not why not just use information from the display? - /** @hide */ - public static boolean isDynamicStack(int stackId) { - return stackId >= FIRST_DYNAMIC_STACK_ID; - } - - /** - * Returns true if we try to maintain focus in the current stack when the top activity - * finishes. - * @hide - */ - // TODO: Figure-out a way to remove. Probably isn't needed in the new world... - public static boolean keepFocusInStackIfPossible(int stackId) { - return stackId == FREEFORM_WORKSPACE_STACK_ID - || stackId == DOCKED_STACK_ID || stackId == PINNED_STACK_ID; - } - - /** - * Returns true if the windows of tasks being moved to the target stack from the source - * stack should be replaced, meaning that window manager will keep the old window around - * until the new is ready. - * @hide - */ - public static boolean replaceWindowsOnTaskMove(int sourceStackId, int targetStackId) { - return sourceStackId == FREEFORM_WORKSPACE_STACK_ID - || targetStackId == FREEFORM_WORKSPACE_STACK_ID; - } - - /** - * Returns true if the top task in the task is allowed to return home when finished and - * there are other tasks in the stack. - * @hide - */ - public static boolean allowTopTaskToReturnHome(int stackId) { - return stackId != PINNED_STACK_ID; - } - - /** - * Returns true if the stack should be resized to match the bounds specified by - * {@link ActivityOptions#setLaunchBounds} when launching an activity into the stack. - * @hide - */ - public static boolean resizeStackWithLaunchBounds(int stackId) { - return stackId == PINNED_STACK_ID; - } - - /** - * Returns true if a window from the specified stack with {@param stackId} are normally - * fullscreen, i. e. they can become the top opaque fullscreen window, meaning that it - * controls system bars, lockscreen occluded/dismissing state, screen rotation animation, - * etc. - * @hide - */ - // TODO: What about the other side of docked stack if we move this to WindowConfiguration? - public static boolean normallyFullscreenWindows(int stackId) { - return stackId != PINNED_STACK_ID && stackId != FREEFORM_WORKSPACE_STACK_ID - && stackId != DOCKED_STACK_ID; - } - - /** Returns the stack id for the input windowing mode. - * @hide */ - // TODO: To be removed once we are not using stack id for stuff... - public static int getStackIdForWindowingMode(int windowingMode) { - switch (windowingMode) { - case WINDOWING_MODE_PINNED: return PINNED_STACK_ID; - case WINDOWING_MODE_FREEFORM: return FREEFORM_WORKSPACE_STACK_ID; - case WINDOWING_MODE_SPLIT_SCREEN_PRIMARY: return DOCKED_STACK_ID; - case WINDOWING_MODE_SPLIT_SCREEN_SECONDARY: return FULLSCREEN_WORKSPACE_STACK_ID; - case WINDOWING_MODE_FULLSCREEN: return FULLSCREEN_WORKSPACE_STACK_ID; - default: return INVALID_STACK_ID; - } - } - - /** Returns the windowing mode that should be used for this input stack id. - * @hide */ - // TODO: To be removed once we are not using stack id for stuff... - public static int getWindowingModeForStackId(int stackId, boolean inSplitScreenMode) { - final int windowingMode; - switch (stackId) { - case FULLSCREEN_WORKSPACE_STACK_ID: - windowingMode = inSplitScreenMode - ? WINDOWING_MODE_SPLIT_SCREEN_SECONDARY : WINDOWING_MODE_FULLSCREEN; - break; - case PINNED_STACK_ID: - windowingMode = WINDOWING_MODE_PINNED; - break; - case DOCKED_STACK_ID: - windowingMode = WINDOWING_MODE_SPLIT_SCREEN_PRIMARY; - break; - case FREEFORM_WORKSPACE_STACK_ID: - windowingMode = WINDOWING_MODE_FREEFORM; - break; - default : - windowingMode = WINDOWING_MODE_UNDEFINED; - } - return windowingMode; - } } /** @@ -1080,11 +942,14 @@ public class ActivityManager { ATTR_TASKDESCRIPTION_PREFIX + "color"; private static final String ATTR_TASKDESCRIPTIONCOLOR_BACKGROUND = ATTR_TASKDESCRIPTION_PREFIX + "colorBackground"; - private static final String ATTR_TASKDESCRIPTIONICONFILENAME = + private static final String ATTR_TASKDESCRIPTIONICON_FILENAME = ATTR_TASKDESCRIPTION_PREFIX + "icon_filename"; + private static final String ATTR_TASKDESCRIPTIONICON_RESOURCE = + ATTR_TASKDESCRIPTION_PREFIX + "icon_resource"; private String mLabel; private Bitmap mIcon; + private int mIconRes; private String mIconFilename; private int mColorPrimary; private int mColorBackground; @@ -1098,9 +963,27 @@ public class ActivityManager { * @param icon An icon that represents the current state of this task. * @param colorPrimary A color to override the theme's primary color. This color must be * opaque. + * @deprecated use TaskDescription constructor with icon resource instead */ + @Deprecated public TaskDescription(String label, Bitmap icon, int colorPrimary) { - this(label, icon, null, colorPrimary, 0, 0, 0); + this(label, icon, 0, null, colorPrimary, 0, 0, 0); + if ((colorPrimary != 0) && (Color.alpha(colorPrimary) != 255)) { + throw new RuntimeException("A TaskDescription's primary color should be opaque"); + } + } + + /** + * Creates the TaskDescription to the specified values. + * + * @param label A label and description of the current state of this task. + * @param iconRes A drawable resource of an icon that represents the current state of this + * activity. + * @param colorPrimary A color to override the theme's primary color. This color must be + * opaque. + */ + public TaskDescription(String label, @DrawableRes int iconRes, int colorPrimary) { + this(label, null, iconRes, null, colorPrimary, 0, 0, 0); if ((colorPrimary != 0) && (Color.alpha(colorPrimary) != 255)) { throw new RuntimeException("A TaskDescription's primary color should be opaque"); } @@ -1111,9 +994,22 @@ public class ActivityManager { * * @param label A label and description of the current state of this activity. * @param icon An icon that represents the current state of this activity. + * @deprecated use TaskDescription constructor with icon resource instead */ + @Deprecated public TaskDescription(String label, Bitmap icon) { - this(label, icon, null, 0, 0, 0, 0); + this(label, icon, 0, null, 0, 0, 0, 0); + } + + /** + * Creates the TaskDescription to the specified values. + * + * @param label A label and description of the current state of this activity. + * @param iconRes A drawable resource of an icon that represents the current state of this + * activity. + */ + public TaskDescription(String label, @DrawableRes int iconRes) { + this(label, null, iconRes, null, 0, 0, 0, 0); } /** @@ -1122,21 +1018,22 @@ public class ActivityManager { * @param label A label and description of the current state of this activity. */ public TaskDescription(String label) { - this(label, null, null, 0, 0, 0, 0); + this(label, null, 0, null, 0, 0, 0, 0); } /** * Creates an empty TaskDescription. */ public TaskDescription() { - this(null, null, null, 0, 0, 0, 0); + this(null, null, 0, null, 0, 0, 0, 0); } /** @hide */ - public TaskDescription(String label, Bitmap icon, String iconFilename, int colorPrimary, - int colorBackground, int statusBarColor, int navigationBarColor) { + public TaskDescription(String label, Bitmap bitmap, int iconRes, String iconFilename, + int colorPrimary, int colorBackground, int statusBarColor, int navigationBarColor) { mLabel = label; - mIcon = icon; + mIcon = bitmap; + mIconRes = iconRes; mIconFilename = iconFilename; mColorPrimary = colorPrimary; mColorBackground = colorBackground; @@ -1158,6 +1055,7 @@ public class ActivityManager { public void copyFrom(TaskDescription other) { mLabel = other.mLabel; mIcon = other.mIcon; + mIconRes = other.mIconRes; mIconFilename = other.mIconFilename; mColorPrimary = other.mColorPrimary; mColorBackground = other.mColorBackground; @@ -1173,6 +1071,7 @@ public class ActivityManager { public void copyFromPreserveHiddenFields(TaskDescription other) { mLabel = other.mLabel; mIcon = other.mIcon; + mIconRes = other.mIconRes; mIconFilename = other.mIconFilename; mColorPrimary = other.mColorPrimary; if (other.mColorBackground != 0) { @@ -1245,6 +1144,14 @@ public class ActivityManager { } /** + * Sets the icon resource for this task description. + * @hide + */ + public void setIcon(int iconRes) { + mIconRes = iconRes; + } + + /** * Moves the icon bitmap reference from an actual Bitmap to a file containing the * bitmap. * @hide @@ -1272,6 +1179,13 @@ public class ActivityManager { } /** @hide */ + @TestApi + public int getIconResource() { + return mIconRes; + } + + /** @hide */ + @TestApi public String getIconFilename() { return mIconFilename; } @@ -1337,7 +1251,10 @@ public class ActivityManager { Integer.toHexString(mColorBackground)); } if (mIconFilename != null) { - out.attribute(null, ATTR_TASKDESCRIPTIONICONFILENAME, mIconFilename); + out.attribute(null, ATTR_TASKDESCRIPTIONICON_FILENAME, mIconFilename); + } + if (mIconRes != 0) { + out.attribute(null, ATTR_TASKDESCRIPTIONICON_RESOURCE, Integer.toString(mIconRes)); } } @@ -1349,8 +1266,10 @@ public class ActivityManager { setPrimaryColor((int) Long.parseLong(attrValue, 16)); } else if (ATTR_TASKDESCRIPTIONCOLOR_BACKGROUND.equals(attrName)) { setBackgroundColor((int) Long.parseLong(attrValue, 16)); - } else if (ATTR_TASKDESCRIPTIONICONFILENAME.equals(attrName)) { + } else if (ATTR_TASKDESCRIPTIONICON_FILENAME.equals(attrName)) { setIconFilename(attrValue); + } else if (ATTR_TASKDESCRIPTIONICON_RESOURCE.equals(attrName)) { + setIcon(Integer.parseInt(attrValue, 10)); } } @@ -1373,6 +1292,7 @@ public class ActivityManager { dest.writeInt(1); mIcon.writeToParcel(dest, 0); } + dest.writeInt(mIconRes); dest.writeInt(mColorPrimary); dest.writeInt(mColorBackground); dest.writeInt(mStatusBarColor); @@ -1388,6 +1308,7 @@ public class ActivityManager { public void readFromParcel(Parcel source) { mLabel = source.readInt() > 0 ? source.readString() : null; mIcon = source.readInt() > 0 ? Bitmap.CREATOR.createFromParcel(source) : null; + mIconRes = source.readInt(); mColorPrimary = source.readInt(); mColorBackground = source.readInt(); mStatusBarColor = source.readInt(); @@ -1408,8 +1329,8 @@ public class ActivityManager { @Override public String toString() { return "TaskDescription Label: " + mLabel + " Icon: " + mIcon + - " IconFilename: " + mIconFilename + " colorPrimary: " + mColorPrimary + - " colorBackground: " + mColorBackground + + " IconRes: " + mIconRes + " IconFilename: " + mIconFilename + + " colorPrimary: " + mColorPrimary + " colorBackground: " + mColorBackground + " statusBarColor: " + mColorBackground + " navigationBarColor: " + mNavigationBarColor; } @@ -1571,7 +1492,6 @@ public class ActivityManager { } dest.writeInt(stackId); dest.writeInt(userId); - dest.writeLong(firstActiveTime); dest.writeLong(lastActiveTime); dest.writeInt(affiliatedTaskId); dest.writeInt(affiliatedTaskColor); @@ -1600,7 +1520,6 @@ public class ActivityManager { TaskDescription.CREATOR.createFromParcel(source) : null; stackId = source.readInt(); userId = source.readInt(); - firstActiveTime = source.readLong(); lastActiveTime = source.readLong(); affiliatedTaskId = source.readInt(); affiliatedTaskColor = source.readInt(); @@ -1643,31 +1562,6 @@ public class ActivityManager { public static final int RECENT_IGNORE_UNAVAILABLE = 0x0002; /** - * Provides a list that contains recent tasks for all - * profiles of a user. - * @hide - */ - public static final int RECENT_INCLUDE_PROFILES = 0x0004; - - /** - * Ignores all tasks that are on the home stack. - * @hide - */ - public static final int RECENT_IGNORE_HOME_AND_RECENTS_STACK_TASKS = 0x0008; - - /** - * Ignores the top task in the docked stack. - * @hide - */ - public static final int RECENT_INGORE_DOCKED_STACK_TOP_TASK = 0x0010; - - /** - * Ignores all tasks that are on the pinned stack. - * @hide - */ - public static final int RECENT_INGORE_PINNED_STACK_TASKS = 0x0020; - - /** * <p></p>Return a list of the tasks that the user has recently launched, with * the most recent being first and older ones after in order. * @@ -1702,33 +1596,7 @@ public class ActivityManager { public List<RecentTaskInfo> getRecentTasks(int maxNum, int flags) throws SecurityException { try { - return getService().getRecentTasks(maxNum, - flags, UserHandle.myUserId()).getList(); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } - - /** - * Same as {@link #getRecentTasks(int, int)} but returns the recent tasks for a - * specific user. It requires holding - * the {@link android.Manifest.permission#INTERACT_ACROSS_USERS_FULL} permission. - * @param maxNum The maximum number of entries to return in the list. The - * actual number returned may be smaller, depending on how many tasks the - * user has started and the maximum number the system can remember. - * @param flags Information about what to return. May be any combination - * of {@link #RECENT_WITH_EXCLUDED} and {@link #RECENT_IGNORE_UNAVAILABLE}. - * - * @return Returns a list of RecentTaskInfo records describing each of - * the recent tasks. Most recently activated tasks go first. - * - * @hide - */ - public List<RecentTaskInfo> getRecentTasksForUser(int maxNum, int flags, int userId) - throws SecurityException { - try { - return getService().getRecentTasks(maxNum, - flags, userId).getList(); + return getService().getRecentTasks(maxNum, flags, UserHandle.myUserId()).getList(); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -2021,22 +1889,6 @@ public class ActivityManager { } /** - * Completely remove the given task. - * - * @param taskId Identifier of the task to be removed. - * @return Returns true if the given task was found and removed. - * - * @hide - */ - public boolean removeTask(int taskId) throws SecurityException { - try { - return getService().removeTask(taskId); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } - - /** * Sets the windowing mode for a specific task. Only works on tasks of type * {@link WindowConfiguration#ACTIVITY_TYPE_STANDARD} * @param taskId The id of the task to set the windowing mode for. diff --git a/android/app/ActivityOptions.java b/android/app/ActivityOptions.java index a68c3a5c..b62e4c2d 100644 --- a/android/app/ActivityOptions.java +++ b/android/app/ActivityOptions.java @@ -17,13 +17,13 @@ package android.app; import static android.app.ActivityManager.DOCKED_STACK_CREATE_MODE_TOP_OR_LEFT; -import static android.app.ActivityManager.StackId.INVALID_STACK_ID; import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.view.Display.INVALID_DISPLAY; import android.annotation.Nullable; import android.annotation.TestApi; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; @@ -159,6 +159,12 @@ public class ActivityOptions { private static final String KEY_ANIM_SPECS = "android:activity.animSpecs"; /** + * Whether the activity should be launched into LockTask mode. + * @see #setLockTaskMode(boolean) + */ + private static final String KEY_LOCK_TASK_MODE = "android:activity.lockTaskMode"; + + /** * The display id the activity should be launched into. * @see #setLaunchDisplayId(int) * @hide @@ -279,6 +285,7 @@ public class ActivityOptions { private int mResultCode; private int mExitCoordinatorIndex; private PendingIntent mUsageTimeReport; + private boolean mLockTaskMode = false; private int mLaunchDisplayId = INVALID_DISPLAY; @WindowConfiguration.WindowingMode private int mLaunchWindowingMode = WINDOWING_MODE_UNDEFINED; @@ -870,6 +877,7 @@ public class ActivityOptions { mExitCoordinatorIndex = opts.getInt(KEY_EXIT_COORDINATOR_INDEX); break; } + mLockTaskMode = opts.getBoolean(KEY_LOCK_TASK_MODE, false); mLaunchDisplayId = opts.getInt(KEY_LAUNCH_DISPLAY_ID, INVALID_DISPLAY); mLaunchWindowingMode = opts.getInt(KEY_LAUNCH_WINDOWING_MODE, WINDOWING_MODE_UNDEFINED); mLaunchActivityType = opts.getInt(KEY_LAUNCH_ACTIVITY_TYPE, ACTIVITY_TYPE_UNDEFINED); @@ -1056,6 +1064,37 @@ public class ActivityOptions { } /** + * Gets whether the activity is to be launched into LockTask mode. + * @return {@code true} if the activity is to be launched into LockTask mode. + * @see Activity#startLockTask() + * @see android.app.admin.DevicePolicyManager#setLockTaskPackages(ComponentName, String[]) + */ + public boolean getLockTaskMode() { + return mLockTaskMode; + } + + /** + * Sets whether the activity is to be launched into LockTask mode. + * + * Use this option to start an activity in LockTask mode. Note that only apps permitted by + * {@link android.app.admin.DevicePolicyManager} can run in LockTask mode. Therefore, if + * {@link android.app.admin.DevicePolicyManager#isLockTaskPermitted(String)} returns + * {@code false} for the package of the target activity, a {@link SecurityException} will be + * thrown during {@link Context#startActivity(Intent, Bundle)}. + * + * Defaults to {@code false} if not set. + * + * @param lockTaskMode {@code true} if the activity is to be launched into LockTask mode. + * @return {@code this} {@link ActivityOptions} instance. + * @see Activity#startLockTask() + * @see android.app.admin.DevicePolicyManager#setLockTaskPackages(ComponentName, String[]) + */ + public ActivityOptions setLockTaskMode(boolean lockTaskMode) { + mLockTaskMode = lockTaskMode; + return this; + } + + /** * Gets the id of the display where activity should be launched. * @return The id of the display where activity should be launched, * {@link android.view.Display#INVALID_DISPLAY} if not set. @@ -1248,6 +1287,7 @@ public class ActivityOptions { mExitCoordinatorIndex = otherOptions.mExitCoordinatorIndex; break; } + mLockTaskMode = otherOptions.mLockTaskMode; mAnimSpecs = otherOptions.mAnimSpecs; mAnimationFinishedListener = otherOptions.mAnimationFinishedListener; mSpecsFuture = otherOptions.mSpecsFuture; @@ -1322,6 +1362,7 @@ public class ActivityOptions { b.putInt(KEY_EXIT_COORDINATOR_INDEX, mExitCoordinatorIndex); break; } + b.putBoolean(KEY_LOCK_TASK_MODE, mLockTaskMode); b.putInt(KEY_LAUNCH_DISPLAY_ID, mLaunchDisplayId); b.putInt(KEY_LAUNCH_WINDOWING_MODE, mLaunchWindowingMode); b.putInt(KEY_LAUNCH_ACTIVITY_TYPE, mLaunchActivityType); diff --git a/android/app/KeyguardManager.java b/android/app/KeyguardManager.java index 54f74b15..1fe29004 100644 --- a/android/app/KeyguardManager.java +++ b/android/app/KeyguardManager.java @@ -387,8 +387,6 @@ public class KeyguardManager { * such as the Home key and the right soft keys, don't work. * * @return true if in keyguard restricted input mode. - * - * @see android.view.WindowManagerPolicy#inKeyguardRestrictedKeyInputMode */ public boolean inKeyguardRestrictedInputMode() { try { diff --git a/android/app/NotificationChannel.java b/android/app/NotificationChannel.java index 47063f08..c06ad3f3 100644 --- a/android/app/NotificationChannel.java +++ b/android/app/NotificationChannel.java @@ -32,6 +32,8 @@ import android.util.proto.ProtoOutputStream; import com.android.internal.util.Preconditions; +import com.android.internal.util.Preconditions; + import org.json.JSONException; import org.json.JSONObject; import org.xmlpull.v1.XmlPullParser; diff --git a/android/app/NotificationManager.java b/android/app/NotificationManager.java index eb52cb7f..a52dc1e4 100644 --- a/android/app/NotificationManager.java +++ b/android/app/NotificationManager.java @@ -934,8 +934,14 @@ public class NotificationManager { public static final int PRIORITY_CATEGORY_CALLS = 1 << 3; /** Calls from repeat callers are prioritized. */ public static final int PRIORITY_CATEGORY_REPEAT_CALLERS = 1 << 4; + /** Alarms are prioritized */ + public static final int PRIORITY_CATEGORY_ALARMS = 1 << 5; + /** Media, system, game (catch-all for non-never suppressible sounds) are prioritized */ + public static final int PRIORITY_CATEGORY_MEDIA_SYSTEM_OTHER = 1 << 6; private static final int[] ALL_PRIORITY_CATEGORIES = { + PRIORITY_CATEGORY_ALARMS, + PRIORITY_CATEGORY_MEDIA_SYSTEM_OTHER, PRIORITY_CATEGORY_REMINDERS, PRIORITY_CATEGORY_EVENTS, PRIORITY_CATEGORY_MESSAGES, @@ -1135,6 +1141,9 @@ public class NotificationManager { case PRIORITY_CATEGORY_MESSAGES: return "PRIORITY_CATEGORY_MESSAGES"; case PRIORITY_CATEGORY_CALLS: return "PRIORITY_CATEGORY_CALLS"; case PRIORITY_CATEGORY_REPEAT_CALLERS: return "PRIORITY_CATEGORY_REPEAT_CALLERS"; + case PRIORITY_CATEGORY_ALARMS: return "PRIORITY_CATEGORY_ALARMS"; + case PRIORITY_CATEGORY_MEDIA_SYSTEM_OTHER: + return "PRIORITY_CATEGORY_MEDIA_SYSTEM_OTHER"; default: return "PRIORITY_CATEGORY_UNKNOWN_" + priorityCategory; } } diff --git a/android/app/StatusBarManager.java b/android/app/StatusBarManager.java index 8987bc02..23c4166d 100644 --- a/android/app/StatusBarManager.java +++ b/android/app/StatusBarManager.java @@ -73,15 +73,16 @@ public class StatusBarManager { public static final int DISABLE2_QUICK_SETTINGS = 1; public static final int DISABLE2_SYSTEM_ICONS = 1 << 1; public static final int DISABLE2_NOTIFICATION_SHADE = 1 << 2; + public static final int DISABLE2_GLOBAL_ACTIONS = 1 << 3; public static final int DISABLE2_NONE = 0x00000000; public static final int DISABLE2_MASK = DISABLE2_QUICK_SETTINGS | DISABLE2_SYSTEM_ICONS - | DISABLE2_NOTIFICATION_SHADE; + | DISABLE2_NOTIFICATION_SHADE | DISABLE2_GLOBAL_ACTIONS; @IntDef(flag = true, value = {DISABLE2_NONE, DISABLE2_MASK, DISABLE2_QUICK_SETTINGS, DISABLE2_SYSTEM_ICONS, - DISABLE2_NOTIFICATION_SHADE}) + DISABLE2_NOTIFICATION_SHADE, DISABLE2_GLOBAL_ACTIONS}) @Retention(RetentionPolicy.SOURCE) public @interface Disable2Flags {} diff --git a/android/app/TaskStackListener.java b/android/app/TaskStackListener.java index 402e2095..895d12a7 100644 --- a/android/app/TaskStackListener.java +++ b/android/app/TaskStackListener.java @@ -77,7 +77,7 @@ public abstract class TaskStackListener extends ITaskStackListener.Stub { } @Override - public void onTaskRemovalStarted(int taskId) { + public void onTaskRemovalStarted(int taskId) throws RemoteException { } @Override @@ -91,11 +91,10 @@ public abstract class TaskStackListener extends ITaskStackListener.Stub { } @Override - public void onTaskProfileLocked(int taskId, int userId) { + public void onTaskProfileLocked(int taskId, int userId) throws RemoteException { } @Override - public void onTaskSnapshotChanged(int taskId, TaskSnapshot snapshot) - throws RemoteException { + public void onTaskSnapshotChanged(int taskId, TaskSnapshot snapshot) throws RemoteException { } } diff --git a/android/app/WallpaperManager.java b/android/app/WallpaperManager.java index 942cc995..081bd814 100644 --- a/android/app/WallpaperManager.java +++ b/android/app/WallpaperManager.java @@ -388,11 +388,12 @@ public class WallpaperManager { public Bitmap peekWallpaperBitmap(Context context, boolean returnDefault, @SetWallpaperFlags int which) { - return peekWallpaperBitmap(context, returnDefault, which, context.getUserId()); + return peekWallpaperBitmap(context, returnDefault, which, context.getUserId(), + false /* hardware */); } public Bitmap peekWallpaperBitmap(Context context, boolean returnDefault, - @SetWallpaperFlags int which, int userId) { + @SetWallpaperFlags int which, int userId, boolean hardware) { if (mService != null) { try { if (!mService.isWallpaperSupported(context.getOpPackageName())) { @@ -409,7 +410,7 @@ public class WallpaperManager { mCachedWallpaper = null; mCachedWallpaperUserId = 0; try { - mCachedWallpaper = getCurrentWallpaperLocked(context, userId); + mCachedWallpaper = getCurrentWallpaperLocked(context, userId, hardware); mCachedWallpaperUserId = userId; } catch (OutOfMemoryError e) { Log.w(TAG, "Out of memory loading the current wallpaper: " + e); @@ -447,7 +448,7 @@ public class WallpaperManager { } } - private Bitmap getCurrentWallpaperLocked(Context context, int userId) { + private Bitmap getCurrentWallpaperLocked(Context context, int userId, boolean hardware) { if (mService == null) { Log.w(TAG, "WallpaperService not running"); return null; @@ -460,6 +461,9 @@ public class WallpaperManager { if (fd != null) { try { BitmapFactory.Options options = new BitmapFactory.Options(); + if (hardware) { + options.inPreferredConfig = Bitmap.Config.HARDWARE; + } return BitmapFactory.decodeFileDescriptor( fd.getFileDescriptor(), null, options); } catch (OutOfMemoryError e) { @@ -814,12 +818,23 @@ public class WallpaperManager { } /** - * Like {@link #getDrawable()} but returns a Bitmap. + * Like {@link #getDrawable()} but returns a Bitmap with default {@link Bitmap.Config}. * * @hide */ public Bitmap getBitmap() { - return getBitmapAsUser(mContext.getUserId()); + return getBitmap(false); + } + + /** + * Like {@link #getDrawable()} but returns a Bitmap. + * + * @param hardware Asks for a hardware backed bitmap. + * @see Bitmap.Config#HARDWARE + * @hide + */ + public Bitmap getBitmap(boolean hardware) { + return getBitmapAsUser(mContext.getUserId(), hardware); } /** @@ -827,8 +842,8 @@ public class WallpaperManager { * * @hide */ - public Bitmap getBitmapAsUser(int userId) { - return sGlobals.peekWallpaperBitmap(mContext, true, FLAG_SYSTEM, userId); + public Bitmap getBitmapAsUser(int userId, boolean hardware) { + return sGlobals.peekWallpaperBitmap(mContext, true, FLAG_SYSTEM, userId, hardware); } /** diff --git a/android/app/WindowConfiguration.java b/android/app/WindowConfiguration.java index 6b405384..251863ca 100644 --- a/android/app/WindowConfiguration.java +++ b/android/app/WindowConfiguration.java @@ -511,7 +511,8 @@ public class WindowConfiguration implements Parcelable, Comparable<WindowConfigu return windowingMode != WINDOWING_MODE_FREEFORM && windowingMode != WINDOWING_MODE_PINNED; } - private static String windowingModeToString(@WindowingMode int windowingMode) { + /** @hide */ + public static String windowingModeToString(@WindowingMode int windowingMode) { switch (windowingMode) { case WINDOWING_MODE_UNDEFINED: return "undefined"; case WINDOWING_MODE_FULLSCREEN: return "fullscreen"; diff --git a/android/app/admin/DevicePolicyManager.java b/android/app/admin/DevicePolicyManager.java index 3c530633..ab8edee7 100644 --- a/android/app/admin/DevicePolicyManager.java +++ b/android/app/admin/DevicePolicyManager.java @@ -6533,6 +6533,52 @@ public class DevicePolicyManager { } /** + * Called by device owner to set the system wall clock time. This only takes effect if called + * when {@link android.provider.Settings.Global#AUTO_TIME} is 0, otherwise {@code false} will be + * returned. + * + * @param admin Which {@link DeviceAdminReceiver} this request is associated with + * @param millis time in milliseconds since the Epoch + * @return {@code true} if set time succeeded, {@code false} otherwise. + * @throws SecurityException if {@code admin} is not a device owner. + */ + public boolean setTime(@NonNull ComponentName admin, long millis) { + throwIfParentInstance("setTime"); + if (mService != null) { + try { + return mService.setTime(admin, millis); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + return false; + } + + /** + * Called by device owner to set the system's persistent default time zone. This only takes + * effect if called when {@link android.provider.Settings.Global#AUTO_TIME_ZONE} is 0, otherwise + * {@code false} will be returned. + * + * @see android.app.AlarmManager#setTimeZone(String) + * @param admin Which {@link DeviceAdminReceiver} this request is associated with + * @param timeZone one of the Olson ids from the list returned by + * {@link java.util.TimeZone#getAvailableIDs} + * @return {@code true} if set timezone succeeded, {@code false} otherwise. + * @throws SecurityException if {@code admin} is not a device owner. + */ + public boolean setTimeZone(@NonNull ComponentName admin, String timeZone) { + throwIfParentInstance("setTimeZone"); + if (mService != null) { + try { + return mService.setTimeZone(admin, timeZone); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + return false; + } + + /** * Called by profile or device owners to update {@link android.provider.Settings.Secure} * settings. Validation that the value of the setting is in the correct form for the setting * type should be performed by the caller. diff --git a/android/app/assist/AssistStructure.java b/android/app/assist/AssistStructure.java index d9b7cd7e..e491a4f9 100644 --- a/android/app/assist/AssistStructure.java +++ b/android/app/assist/AssistStructure.java @@ -616,6 +616,9 @@ public class AssistStructure implements Parcelable { CharSequence[] mAutofillOptions; boolean mSanitized; HtmlInfo mHtmlInfo; + int mMinEms = -1; + int mMaxEms = -1; + int mMaxLength = -1; // POJO used to override some autofill-related values when the node is parcelized. // Not written to parcel. @@ -713,6 +716,9 @@ public class AssistStructure implements Parcelable { if (p instanceof HtmlInfo) { mHtmlInfo = (HtmlInfo) p; } + mMinEms = in.readInt(); + mMaxEms = in.readInt(); + mMaxLength = in.readInt(); } if ((flags&FLAGS_HAS_LARGE_COORDS) != 0) { mX = in.readInt(); @@ -876,6 +882,9 @@ public class AssistStructure implements Parcelable { } else { out.writeParcelable(null, 0); } + out.writeInt(mMinEms); + out.writeInt(mMaxEms); + out.writeInt(mMaxLength); } if ((flags&FLAGS_HAS_LARGE_COORDS) != 0) { out.writeInt(mX); @@ -1444,6 +1453,39 @@ public class AssistStructure implements Parcelable { public ViewNode getChildAt(int index) { return mChildren[index]; } + + /** + * Returns the minimum width in ems of the text associated with this node, or {@code -1} + * if not supported by the node. + * + * <p>It's only relevant when the {@link AssistStructure} is used for autofill purposes, + * not for assist purposes. + */ + public int getMinTextEms() { + return mMinEms; + } + + /** + * Returns the maximum width in ems of the text associated with this node, or {@code -1} + * if not supported by the node. + * + * <p>It's only relevant when the {@link AssistStructure} is used for autofill purposes, + * not for assist purposes. + */ + public int getMaxTextEms() { + return mMaxEms; + } + + /** + * Returns the maximum length of the text associated with this node node, or {@code -1} + * if not supported by the node or not set. + * + * <p>It's only relevant when the {@link AssistStructure} is used for autofill purposes, + * not for assist purposes. + */ + public int getMaxTextLength() { + return mMaxLength; + } } /** @@ -1776,6 +1818,21 @@ public class AssistStructure implements Parcelable { } @Override + public void setMinTextEms(int minEms) { + mNode.mMinEms = minEms; + } + + @Override + public void setMaxTextEms(int maxEms) { + mNode.mMaxEms = maxEms; + } + + @Override + public void setMaxTextLength(int maxLength) { + mNode.mMaxLength = maxLength; + } + + @Override public void setDataIsSensitive(boolean sensitive) { mNode.mSanitized = !sensitive; } diff --git a/android/app/job/JobScheduler.java b/android/app/job/JobScheduler.java index 3868439f..0deb2e13 100644 --- a/android/app/job/JobScheduler.java +++ b/android/app/job/JobScheduler.java @@ -24,7 +24,6 @@ import android.annotation.SystemApi; import android.annotation.SystemService; import android.content.ClipData; import android.content.Context; -import android.content.Intent; import android.os.Bundle; import android.os.PersistableBundle; @@ -40,16 +39,18 @@ import java.util.List; * and how to construct them. You will construct these JobInfo objects and pass them to the * JobScheduler with {@link #schedule(JobInfo)}. When the criteria declared are met, the * system will execute this job on your application's {@link android.app.job.JobService}. - * You identify which JobService is meant to execute the logic for your job when you create the - * JobInfo with + * You identify the service component that implements the logic for your job when you + * construct the JobInfo using * {@link android.app.job.JobInfo.Builder#JobInfo.Builder(int,android.content.ComponentName)}. * </p> * <p> - * The framework will be intelligent about when you receive your callbacks, and attempt to batch - * and defer them as much as possible. Typically if you don't specify a deadline on your job, it - * can be run at any moment depending on the current state of the JobScheduler's internal queue, - * however it might be deferred as long as until the next time the device is connected to a power - * source. + * The framework will be intelligent about when it executes jobs, and attempt to batch + * and defer them as much as possible. Typically if you don't specify a deadline on a job, it + * can be run at any moment depending on the current state of the JobScheduler's internal queue. + * <p> + * While a job is running, the system holds a wakelock on behalf of your app. For this reason, + * you do not need to take any action to guarantee that the device stays awake for the + * duration of the job. * </p> * <p>You do not * instantiate this class directly; instead, retrieve it through @@ -141,30 +142,34 @@ public abstract class JobScheduler { int userId, String tag); /** - * Cancel a job that is pending in the JobScheduler. - * @param jobId unique identifier for this job. Obtain this value from the jobs returned by - * {@link #getAllPendingJobs()}. + * Cancel the specified job. If the job is currently executing, it is stopped + * immediately and the return value from its {@link JobService#onStopJob(JobParameters)} + * method is ignored. + * + * @param jobId unique identifier for the job to be canceled, as supplied to + * {@link JobInfo.Builder#JobInfo.Builder(int, android.content.ComponentName) + * JobInfo.Builder(int, android.content.ComponentName)}. */ public abstract void cancel(int jobId); /** - * Cancel all jobs that have been registered with the JobScheduler by this package. + * Cancel <em>all</em> jobs that have been scheduled by the calling application. */ public abstract void cancelAll(); /** - * Retrieve all jobs for this package that are pending in the JobScheduler. + * Retrieve all jobs that have been scheduled by the calling application. * - * @return a list of all the jobs registered by this package that have not - * yet been executed. + * @return a list of all of the app's scheduled jobs. This includes jobs that are + * currently started as well as those that are still waiting to run. */ public abstract @NonNull List<JobInfo> getAllPendingJobs(); /** - * Retrieve a specific job for this package that is pending in the - * JobScheduler. + * Look up the description of a scheduled job. * - * @return job registered by this package that has not yet been executed. + * @return The {@link JobInfo} description of the given scheduled job, or {@code null} + * if the supplied job ID does not correspond to any job. */ public abstract @Nullable JobInfo getPendingJob(int jobId); } diff --git a/android/app/job/JobService.java b/android/app/job/JobService.java index 9096b47b..69afed20 100644 --- a/android/app/job/JobService.java +++ b/android/app/job/JobService.java @@ -18,16 +18,7 @@ package android.app.job; import android.app.Service; import android.content.Intent; -import android.os.Handler; import android.os.IBinder; -import android.os.Looper; -import android.os.Message; -import android.os.RemoteException; -import android.util.Log; - -import com.android.internal.annotations.GuardedBy; - -import java.lang.ref.WeakReference; /** * <p>Entry point for the callback from the {@link android.app.job.JobScheduler}.</p> @@ -55,7 +46,7 @@ public abstract class JobService extends Service { * </pre> * * <p>If a job service is declared in the manifest but not protected with this - * permission, that service will be ignored by the OS. + * permission, that service will be ignored by the system. */ public static final String PERMISSION_BIND = "android.permission.BIND_JOB_SERVICE"; @@ -81,14 +72,36 @@ public abstract class JobService extends Service { } /** - * Override this method with the callback logic for your job. Any such logic needs to be - * performed on a separate thread, as this function is executed on your application's main - * thread. + * Called to indicate that the job has begun executing. Override this method with the + * logic for your job. Like all other component lifecycle callbacks, this method executes + * on your application's main thread. + * <p> + * Return {@code true} from this method if your job needs to continue running. If you + * do this, the job remains active until you call + * {@link #jobFinished(JobParameters, boolean)} to tell the system that it has completed + * its work, or until the job's required constraints are no longer satisfied. For + * example, if the job was scheduled using + * {@link JobInfo.Builder#setRequiresCharging(boolean) setRequiresCharging(true)}, + * it will be immediately halted by the system if the user unplugs the device from power, + * the job's {@link #onStopJob(JobParameters)} callback will be invoked, and the app + * will be expected to shut down all ongoing work connected with that job. + * <p> + * The system holds a wakelock on behalf of your app as long as your job is executing. + * This wakelock is acquired before this method is invoked, and is not released until either + * you call {@link #jobFinished(JobParameters, boolean)}, or after the system invokes + * {@link #onStopJob(JobParameters)} to notify your job that it is being shut down + * prematurely. + * <p> + * Returning {@code false} from this method means your job is already finished. The + * system's wakelock for the job will be released, and {@link #onStopJob(JobParameters)} + * will not be invoked. * - * @param params Parameters specifying info about this job, including the extras bundle you - * optionally provided at job-creation time. - * @return True if your service needs to process the work (on a separate thread). False if - * there's no more work to be done for this job. + * @param params Parameters specifying info about this job, including the optional + * extras configured with {@link JobInfo.Builder#setExtras(android.os.PersistableBundle). + * This object serves to identify this specific running job instance when calling + * {@link #jobFinished(JobParameters, boolean)}. + * @return {@code true} if your service will continue running, using a separate thread + * when appropriate. {@code false} means that this job has completed its work. */ public abstract boolean onStartJob(JobParameters params); @@ -101,37 +114,44 @@ public abstract class JobService extends Service { * {@link android.app.job.JobInfo.Builder#setRequiredNetworkType(int)}, yet while your * job was executing the user toggled WiFi. Another example is if you had specified * {@link android.app.job.JobInfo.Builder#setRequiresDeviceIdle(boolean)}, and the phone left its - * idle maintenance window. You are solely responsible for the behaviour of your application - * upon receipt of this message; your app will likely start to misbehave if you ignore it. One - * immediate repercussion is that the system will cease holding a wakelock for you.</p> + * idle maintenance window. You are solely responsible for the behavior of your application + * upon receipt of this message; your app will likely start to misbehave if you ignore it. + * <p> + * Once this method returns, the system releases the wakelock that it is holding on + * behalf of the job.</p> * - * @param params Parameters specifying info about this job. - * @return True to indicate to the JobManager whether you'd like to reschedule this job based - * on the retry criteria provided at job creation-time. False to drop the job. Regardless of - * the value returned, your job must stop executing. + * @param params The parameters identifying this job, as supplied to + * the job in the {@link #onStartJob(JobParameters)} callback. + * @return {@code true} to indicate to the JobManager whether you'd like to reschedule + * this job based on the retry criteria provided at job creation-time; or {@code false} + * to end the job entirely. Regardless of the value returned, your job must stop executing. */ public abstract boolean onStopJob(JobParameters params); /** - * Call this to inform the JobManager you've finished executing. This can be called from any - * thread, as it will ultimately be run on your application's main thread. When the system - * receives this message it will release the wakelock being held. + * Call this to inform the JobScheduler that the job has finished its work. When the + * system receives this message, it releases the wakelock being held for the job. * <p> - * You can specify post-execution behaviour to the scheduler here with - * <code>needsReschedule </code>. This will apply a back-off timer to your job based on - * the default, or what was set with - * {@link android.app.job.JobInfo.Builder#setBackoffCriteria(long, int)}. The original - * requirements are always honoured even for a backed-off job. Note that a job running in - * idle mode will not be backed-off. Instead what will happen is the job will be re-added - * to the queue and re-executed within a future idle maintenance window. + * You can request that the job be scheduled again by passing {@code true} as + * the <code>wantsReschedule</code> parameter. This will apply back-off policy + * for the job; this policy can be adjusted through the + * {@link android.app.job.JobInfo.Builder#setBackoffCriteria(long, int)} method + * when the job is originally scheduled. The job's initial + * requirements are preserved when jobs are rescheduled, regardless of backed-off + * policy. + * <p class="note"> + * A job running while the device is dozing will not be rescheduled with the normal back-off + * policy. Instead, the job will be re-added to the queue and executed again during + * a future idle maintenance window. * </p> * - * @param params Parameters specifying system-provided info about this job, this was given to - * your application in {@link #onStartJob(JobParameters)}. - * @param needsReschedule True if this job should be rescheduled according to the back-off - * criteria specified at schedule-time. False otherwise. + * @param params The parameters identifying this job, as supplied to + * the job in the {@link #onStartJob(JobParameters)} callback. + * @param wantsReschedule {@code true} if this job should be rescheduled according + * to the back-off criteria specified when it was first scheduled; {@code false} + * otherwise. */ - public final void jobFinished(JobParameters params, boolean needsReschedule) { - mEngine.jobFinished(params, needsReschedule); + public final void jobFinished(JobParameters params, boolean wantsReschedule) { + mEngine.jobFinished(params, wantsReschedule); } -}
\ No newline at end of file +} diff --git a/android/app/slice/Slice.java b/android/app/slice/Slice.java new file mode 100644 index 00000000..7f9f74b4 --- /dev/null +++ b/android/app/slice/Slice.java @@ -0,0 +1,417 @@ +/* + * 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; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.StringDef; +import android.app.PendingIntent; +import android.app.RemoteInput; +import android.content.ContentResolver; +import android.content.IContentProvider; +import android.graphics.drawable.Icon; +import android.net.Uri; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.RemoteException; +import android.widget.RemoteViews; + +import com.android.internal.util.ArrayUtils; +import com.android.internal.util.Preconditions; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * A slice is a piece of app content and actions that can be surfaced outside of the app. + * + * <p>They are constructed using {@link Builder} in a tree structure + * that provides the OS some information about how the content should be displayed. + */ +public final class Slice implements Parcelable { + + /** + * @hide + */ + @StringDef({HINT_TITLE, HINT_LIST, HINT_LIST_ITEM, HINT_LARGE, HINT_ACTIONS, HINT_SELECTED, + HINT_SOURCE, HINT_MESSAGE, HINT_HORIZONTAL, HINT_NO_TINT, HINT_PARTIAL}) + public @interface SliceHint{ } + + /** + * Hint that this content is a title of other content in the slice. + */ + public static final String HINT_TITLE = "title"; + /** + * Hint that all sub-items/sub-slices within this content should be considered + * to have {@link #HINT_LIST_ITEM}. + */ + public static final String HINT_LIST = "list"; + /** + * Hint that this item is part of a list and should be formatted as if is part + * of a list. + */ + public static final String HINT_LIST_ITEM = "list_item"; + /** + * Hint that this content is important and should be larger when displayed if + * possible. + */ + public static final String HINT_LARGE = "large"; + /** + * Hint that this slice contains a number of actions that can be grouped together + * in a sort of controls area of the UI. + */ + public static final String HINT_ACTIONS = "actions"; + /** + * Hint indicating that this item (and its sub-items) are the current selection. + */ + public static final String HINT_SELECTED = "selected"; + /** + * Hint to indicate that this is a message as part of a communication + * sequence in this slice. + */ + public static final String HINT_MESSAGE = "message"; + /** + * Hint to tag the source (i.e. sender) of a {@link #HINT_MESSAGE}. + */ + public static final String HINT_SOURCE = "source"; + /** + * Hint that list items within this slice or subslice would appear better + * if organized horizontally. + */ + public static final String HINT_HORIZONTAL = "horizontal"; + /** + * Hint to indicate that this content should not be tinted. + */ + public static final String HINT_NO_TINT = "no_tint"; + /** + * Hint to indicate that this slice is incomplete and an update will be sent once + * loading is complete. Slices which contain HINT_PARTIAL will not be cached by the + * OS and should not be cached by apps. + */ + public static final String HINT_PARTIAL = "partial"; + + // These two are coming over from prototyping, but we probably don't want in + // public API, at least not right now. + /** + * @hide + */ + public static final String HINT_ALT = "alt"; + + private final SliceItem[] mItems; + private final @SliceHint String[] mHints; + private Uri mUri; + + Slice(ArrayList<SliceItem> items, @SliceHint String[] hints, Uri uri) { + mHints = hints; + mItems = items.toArray(new SliceItem[items.size()]); + mUri = uri; + } + + protected Slice(Parcel in) { + mHints = in.readStringArray(); + int n = in.readInt(); + mItems = new SliceItem[n]; + for (int i = 0; i < n; i++) { + mItems[i] = SliceItem.CREATOR.createFromParcel(in); + } + mUri = Uri.CREATOR.createFromParcel(in); + } + + /** + * @return The Uri that this Slice represents. + */ + public Uri getUri() { + return mUri; + } + + /** + * @return All child {@link SliceItem}s that this Slice contains. + */ + public List<SliceItem> getItems() { + return Arrays.asList(mItems); + } + + /** + * @return All hints associated with this Slice. + */ + public @SliceHint List<String> getHints() { + 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); + dest.writeInt(mItems.length); + for (int i = 0; i < mItems.length; i++) { + mItems[i].writeToParcel(dest, flags); + } + mUri.writeToParcel(dest, 0); + } + + @Override + public int describeContents() { + return 0; + } + + /** + * @hide + */ + public boolean hasHint(@SliceHint String hint) { + return ArrayUtils.contains(mHints, hint); + } + + /** + * A Builder used to construct {@link Slice}s + */ + public static class Builder { + + private final Uri mUri; + private ArrayList<SliceItem> mItems = new ArrayList<>(); + private @SliceHint ArrayList<String> mHints = new ArrayList<>(); + + /** + * Create a builder which will construct a {@link Slice} for the Given Uri. + * @param uri Uri to tag for this slice. + */ + public Builder(@NonNull Uri uri) { + mUri = uri; + } + + /** + * Create a builder for a {@link Slice} that is a sub-slice of the slice + * being constructed by the provided builder. + * @param parent The builder constructing the parent slice + */ + public Builder(@NonNull Slice.Builder parent) { + mUri = parent.mUri.buildUpon().appendPath("_gen") + .appendPath(String.valueOf(mItems.size())).build(); + } + + /** + * Add hints to the Slice being constructed + */ + public Builder addHints(@SliceHint String... hints) { + mHints.addAll(Arrays.asList(hints)); + return this; + } + + /** + * Add hints to the Slice being constructed + */ + public Builder addHints(@SliceHint List<String> hints) { + return addHints(hints.toArray(new String[hints.size()])); + } + + /** + * Add a sub-slice to the slice being constructed + */ + public Builder addSubSlice(@NonNull Slice slice) { + mItems.add(new SliceItem(slice, SliceItem.TYPE_SLICE, slice.getHints().toArray( + new String[slice.getHints().size()]))); + return this; + } + + /** + * Add an action to the slice being constructed + */ + public Slice.Builder addAction(@NonNull PendingIntent action, @NonNull Slice s) { + mItems.add(new SliceItem(action, s, SliceItem.TYPE_ACTION, new String[0])); + return this; + } + + /** + * Add text to the slice being constructed + */ + public Builder addText(CharSequence text, @SliceHint String... hints) { + mItems.add(new SliceItem(text, SliceItem.TYPE_TEXT, hints)); + return this; + } + + /** + * Add text to the slice being constructed + */ + public Builder addText(CharSequence text, @SliceHint List<String> hints) { + return addText(text, hints.toArray(new String[hints.size()])); + } + + /** + * Add an image to the slice being constructed + */ + public Builder addIcon(Icon icon, @SliceHint String... hints) { + mItems.add(new SliceItem(icon, SliceItem.TYPE_IMAGE, hints)); + return this; + } + + /** + * Add an image to the slice being constructed + */ + public Builder addIcon(Icon icon, @SliceHint List<String> hints) { + return addIcon(icon, hints.toArray(new String[hints.size()])); + } + + /** + * @hide This isn't final + */ + public Builder addRemoteView(RemoteViews remoteView, @SliceHint String... hints) { + mItems.add(new SliceItem(remoteView, SliceItem.TYPE_REMOTE_VIEW, hints)); + return this; + } + + /** + * Add remote input to the slice being constructed + */ + public Slice.Builder addRemoteInput(RemoteInput remoteInput, + @SliceHint List<String> hints) { + return addRemoteInput(remoteInput, hints.toArray(new String[hints.size()])); + } + + /** + * Add remote input to the slice being constructed + */ + public Slice.Builder addRemoteInput(RemoteInput remoteInput, @SliceHint String... hints) { + mItems.add(new SliceItem(remoteInput, SliceItem.TYPE_REMOTE_INPUT, hints)); + return this; + } + + /** + * Add a color to the slice being constructed + */ + public Builder addColor(int color, @SliceHint String... hints) { + mItems.add(new SliceItem(color, SliceItem.TYPE_COLOR, hints)); + return this; + } + + /** + * Add a color to the slice being constructed + */ + public Builder addColor(int color, @SliceHint List<String> hints) { + return addColor(color, hints.toArray(new String[hints.size()])); + } + + /** + * Add a timestamp to the slice being constructed + */ + public Slice.Builder addTimestamp(long time, @SliceHint String... hints) { + mItems.add(new SliceItem(time, SliceItem.TYPE_TIMESTAMP, hints)); + return this; + } + + /** + * Add a timestamp to the slice being constructed + */ + public Slice.Builder addTimestamp(long time, @SliceHint List<String> hints) { + return addTimestamp(time, hints.toArray(new String[hints.size()])); + } + + /** + * Construct the slice. + */ + public Slice build() { + return new Slice(mItems, mHints.toArray(new String[mHints.size()]), mUri); + } + } + + public static final Creator<Slice> CREATOR = new Creator<Slice>() { + @Override + public Slice createFromParcel(Parcel in) { + return new Slice(in); + } + + @Override + public Slice[] newArray(int size) { + return new Slice[size]; + } + }; + + /** + * @hide + * @return A string representation of this slice. + */ + public String toString() { + return toString(""); + } + + private String toString(String indent) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < mItems.length; i++) { + sb.append(indent); + if (mItems[i].getType() == SliceItem.TYPE_SLICE) { + sb.append("slice:\n"); + sb.append(mItems[i].getSlice().toString(indent + " ")); + } else if (mItems[i].getType() == SliceItem.TYPE_TEXT) { + sb.append("text: "); + sb.append(mItems[i].getText()); + sb.append("\n"); + } else { + sb.append(SliceItem.typeToString(mItems[i].getType())); + sb.append("\n"); + } + } + return sb.toString(); + } + + /** + * Turns a slice Uri into slice content. + * + * @param resolver ContentResolver to be used. + * @param uri The URI to a slice provider + * @return The Slice provided by the app or null if none is given. + * @see Slice + */ + public static @Nullable Slice bindSlice(ContentResolver resolver, @NonNull Uri uri) { + Preconditions.checkNotNull(uri, "uri"); + IContentProvider provider = resolver.acquireProvider(uri); + if (provider == null) { + throw new IllegalArgumentException("Unknown URI " + uri); + } + try { + Bundle extras = new Bundle(); + extras.putParcelable(SliceProvider.EXTRA_BIND_URI, uri); + final Bundle res = provider.call(resolver.getPackageName(), SliceProvider.METHOD_SLICE, + null, extras); + Bundle.setDefusable(res, true); + return res.getParcelable(SliceProvider.EXTRA_SLICE); + } catch (RemoteException e) { + // Arbitrary and not worth documenting, as Activity + // Manager will kill this process shortly anyway. + return null; + } finally { + resolver.releaseProvider(provider); + } + } +} diff --git a/android/app/slice/SliceItem.java b/android/app/slice/SliceItem.java new file mode 100644 index 00000000..6e69b051 --- /dev/null +++ b/android/app/slice/SliceItem.java @@ -0,0 +1,346 @@ +/* + * 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; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.app.PendingIntent; +import android.app.RemoteInput; +import android.graphics.drawable.Icon; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; +import android.util.Pair; +import android.widget.RemoteViews; + +import com.android.internal.util.ArrayUtils; + +import java.util.Arrays; +import java.util.List; + + +/** + * A SliceItem is a single unit in the tree structure of a {@link Slice}. + * + * A SliceItem a piece of content and some hints about what that content + * means or how it should be displayed. The types of content can be: + * <li>{@link #TYPE_SLICE}</li> + * <li>{@link #TYPE_TEXT}</li> + * <li>{@link #TYPE_IMAGE}</li> + * <li>{@link #TYPE_ACTION}</li> + * <li>{@link #TYPE_COLOR}</li> + * <li>{@link #TYPE_TIMESTAMP}</li> + * <li>{@link #TYPE_REMOTE_INPUT}</li> + * + * The hints that a {@link SliceItem} are a set of strings which annotate + * the content. The hints that are guaranteed to be understood by the system + * are defined on {@link Slice}. + */ +public final class SliceItem implements Parcelable { + + /** + * @hide + */ + @IntDef({TYPE_SLICE, TYPE_TEXT, TYPE_IMAGE, TYPE_ACTION, TYPE_COLOR, + TYPE_TIMESTAMP, TYPE_REMOTE_INPUT}) + public @interface SliceType {} + + /** + * A {@link SliceItem} that contains a {@link Slice} + */ + public static final int TYPE_SLICE = 1; + /** + * A {@link SliceItem} that contains a {@link CharSequence} + */ + public static final int TYPE_TEXT = 2; + /** + * A {@link SliceItem} that contains an {@link Icon} + */ + public static final int TYPE_IMAGE = 3; + /** + * A {@link SliceItem} that contains a {@link PendingIntent} + * + * Note: Actions contain 2 pieces of data, In addition to the pending intent, the + * item contains a {@link Slice} that the action applies to. + */ + public static final int TYPE_ACTION = 4; + /** + * @hide This isn't final + */ + public static final int TYPE_REMOTE_VIEW = 5; + /** + * A {@link SliceItem} that contains a Color int. + */ + public static final int TYPE_COLOR = 6; + /** + * A {@link SliceItem} that contains a timestamp. + */ + public static final int TYPE_TIMESTAMP = 8; + /** + * A {@link SliceItem} that contains a {@link RemoteInput}. + */ + public static final int TYPE_REMOTE_INPUT = 9; + + /** + * @hide + */ + protected @Slice.SliceHint + String[] mHints; + private final int mType; + private final Object mObj; + + /** + * @hide + */ + public SliceItem(Object obj, @SliceType int type, @Slice.SliceHint String[] hints) { + mHints = hints; + mType = type; + mObj = obj; + } + + /** + * @hide + */ + public SliceItem(PendingIntent intent, Slice slice, int type, @Slice.SliceHint String[] hints) { + this(new Pair<>(intent, slice), type, hints); + } + + /** + * Gets all hints associated with this SliceItem. + * @return Array of hints. + */ + public @NonNull @Slice.SliceHint List<String> getHints() { + return Arrays.asList(mHints); + } + + /** + * @hide + */ + public void addHint(@Slice.SliceHint String hint) { + mHints = ArrayUtils.appendElement(String.class, mHints, hint); + } + + /** + * @hide + */ + public void removeHint(String hint) { + ArrayUtils.removeElement(String.class, mHints, hint); + } + + public @SliceType int getType() { + return mType; + } + + /** + * @return The text held by this {@link #TYPE_TEXT} SliceItem + */ + public CharSequence getText() { + return (CharSequence) mObj; + } + + /** + * @return The icon held by this {@link #TYPE_IMAGE} SliceItem + */ + public Icon getIcon() { + return (Icon) mObj; + } + + /** + * @return The pending intent held by this {@link #TYPE_ACTION} SliceItem + */ + public PendingIntent getAction() { + return ((Pair<PendingIntent, Slice>) mObj).first; + } + + /** + * @hide This isn't final + */ + public RemoteViews getRemoteView() { + return (RemoteViews) mObj; + } + + /** + * @return The remote input held by this {@link #TYPE_REMOTE_INPUT} SliceItem + */ + public RemoteInput getRemoteInput() { + return (RemoteInput) mObj; + } + + /** + * @return The color held by this {@link #TYPE_COLOR} SliceItem + */ + public int getColor() { + return (Integer) mObj; + } + + /** + * @return The slice held by this {@link #TYPE_ACTION} or {@link #TYPE_SLICE} SliceItem + */ + public Slice getSlice() { + if (getType() == TYPE_ACTION) { + return ((Pair<PendingIntent, Slice>) mObj).second; + } + return (Slice) mObj; + } + + /** + * @return The timestamp held by this {@link #TYPE_TIMESTAMP} SliceItem + */ + public long getTimestamp() { + return (Long) mObj; + } + + /** + * @param hint The hint to check for + * @return true if this item contains the given hint + */ + public boolean hasHint(@Slice.SliceHint String hint) { + return ArrayUtils.contains(mHints, hint); + } + + /** + * @hide + */ + public SliceItem(Parcel in) { + mHints = in.readStringArray(); + mType = in.readInt(); + mObj = readObj(mType, in); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeStringArray(mHints); + dest.writeInt(mType); + writeObj(dest, flags, mObj, mType); + } + + /** + * @hide + */ + public boolean hasHints(@Slice.SliceHint String[] hints) { + if (hints == null) return true; + for (String hint : hints) { + if (!TextUtils.isEmpty(hint) && !ArrayUtils.contains(mHints, hint)) { + return false; + } + } + return true; + } + + /** + * @hide + */ + public boolean hasAnyHints(@Slice.SliceHint String[] hints) { + if (hints == null) return false; + for (String hint : hints) { + if (ArrayUtils.contains(mHints, hint)) { + return true; + } + } + return false; + } + + private void writeObj(Parcel dest, int flags, Object obj, int type) { + switch (type) { + case TYPE_SLICE: + case TYPE_REMOTE_VIEW: + case TYPE_IMAGE: + case TYPE_REMOTE_INPUT: + ((Parcelable) obj).writeToParcel(dest, flags); + break; + case TYPE_ACTION: + ((Pair<PendingIntent, Slice>) obj).first.writeToParcel(dest, flags); + ((Pair<PendingIntent, Slice>) obj).second.writeToParcel(dest, flags); + break; + case TYPE_TEXT: + TextUtils.writeToParcel((CharSequence) mObj, dest, flags); + break; + case TYPE_COLOR: + dest.writeInt((Integer) mObj); + break; + case TYPE_TIMESTAMP: + dest.writeLong((Long) mObj); + break; + } + } + + private static Object readObj(int type, Parcel in) { + switch (type) { + case TYPE_SLICE: + return Slice.CREATOR.createFromParcel(in); + case TYPE_TEXT: + return TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); + case TYPE_IMAGE: + return Icon.CREATOR.createFromParcel(in); + case TYPE_ACTION: + return new Pair<PendingIntent, Slice>( + PendingIntent.CREATOR.createFromParcel(in), + Slice.CREATOR.createFromParcel(in)); + case TYPE_REMOTE_VIEW: + return RemoteViews.CREATOR.createFromParcel(in); + case TYPE_COLOR: + return in.readInt(); + case TYPE_TIMESTAMP: + return in.readLong(); + case TYPE_REMOTE_INPUT: + return RemoteInput.CREATOR.createFromParcel(in); + } + throw new RuntimeException("Unsupported type " + type); + } + + public static final Creator<SliceItem> CREATOR = new Creator<SliceItem>() { + @Override + public SliceItem createFromParcel(Parcel in) { + return new SliceItem(in); + } + + @Override + public SliceItem[] newArray(int size) { + return new SliceItem[size]; + } + }; + + /** + * @hide + */ + public static String typeToString(int type) { + switch (type) { + case TYPE_SLICE: + return "Slice"; + case TYPE_TEXT: + return "Text"; + case TYPE_IMAGE: + return "Image"; + case TYPE_ACTION: + return "Action"; + case TYPE_REMOTE_VIEW: + return "RemoteView"; + case TYPE_COLOR: + return "Color"; + case TYPE_TIMESTAMP: + return "Timestamp"; + case TYPE_REMOTE_INPUT: + return "RemoteInput"; + } + return "Unrecognized type: " + type; + } +} diff --git a/android/app/slice/SliceProvider.java b/android/app/slice/SliceProvider.java new file mode 100644 index 00000000..df87b455 --- /dev/null +++ b/android/app/slice/SliceProvider.java @@ -0,0 +1,182 @@ +/* + * 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; + +import android.Manifest.permission; +import android.content.ContentProvider; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.database.ContentObserver; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.os.CancellationSignal; +import android.os.Handler; +import android.os.Looper; +import android.os.StrictMode; +import android.os.StrictMode.ThreadPolicy; +import android.util.Log; + +import java.util.concurrent.CountDownLatch; + +/** + * A SliceProvider allows app to provide content to be displayed in system + * spaces. This content is templated and can contain actions, and the behavior + * of how it is surfaced is specific to the system surface. + * + * <p>Slices are not currently live content. They are bound once and shown to the + * user. If the content changes due to a callback from user interaction, then + * {@link ContentResolver#notifyChange(Uri, ContentObserver)} + * should be used to notify the system.</p> + * + * <p>The provider needs to be declared in the manifest to provide the authority + * for the app. The authority for most slices is expected to match the package + * of the application.</p> + * <pre class="prettyprint"> + * {@literal + * <provider + * android:name="com.android.mypkg.MySliceProvider" + * android:authorities="com.android.mypkg" />} + * </pre> + * + * @see Slice + */ +public abstract class SliceProvider extends ContentProvider { + + /** + * This is the Android platform's MIME type for a slice: URI + * containing a slice implemented through {@link SliceProvider}. + */ + public static final String SLICE_TYPE = "vnd.android.slice"; + + private static final String TAG = "SliceProvider"; + /** + * @hide + */ + public static final String EXTRA_BIND_URI = "slice_uri"; + /** + * @hide + */ + public static final String METHOD_SLICE = "bind_slice"; + /** + * @hide + */ + public static final String EXTRA_SLICE = "slice"; + + private static final boolean DEBUG = false; + + /** + * Implemented to create a slice. Will be called on the main thread. + * <p> + * onBindSlice should return as quickly as possible so that the UI tied + * to this slice can be responsive. No network or other IO will be allowed + * during onBindSlice. Any loading that needs to be done should happen + * off the main thread with a call to {@link ContentResolver#notifyChange(Uri, ContentObserver)} + * when the app is ready to provide the complete data in onBindSlice. + * <p> + * + * @see {@link Slice}. + * @see {@link Slice#HINT_PARTIAL} + */ + // TODO: Provide alternate notifyChange that takes in the slice (i.e. notifyChange(Uri, Slice)). + public abstract Slice onBindSlice(Uri sliceUri); + + @Override + public final int update(Uri uri, ContentValues values, String selection, + String[] selectionArgs) { + if (DEBUG) Log.d(TAG, "update " + uri); + return 0; + } + + @Override + public final int delete(Uri uri, String selection, String[] selectionArgs) { + if (DEBUG) Log.d(TAG, "delete " + uri); + return 0; + } + + @Override + public final Cursor query(Uri uri, String[] projection, String selection, + String[] selectionArgs, String sortOrder) { + if (DEBUG) Log.d(TAG, "query " + uri); + return null; + } + + @Override + public final Cursor query(Uri uri, String[] projection, String selection, String[] + selectionArgs, String sortOrder, CancellationSignal cancellationSignal) { + if (DEBUG) Log.d(TAG, "query " + uri); + return null; + } + + @Override + public final Cursor query(Uri uri, String[] projection, Bundle queryArgs, + CancellationSignal cancellationSignal) { + if (DEBUG) Log.d(TAG, "query " + uri); + return null; + } + + @Override + public final Uri insert(Uri uri, ContentValues values) { + if (DEBUG) Log.d(TAG, "insert " + uri); + return null; + } + + @Override + public final String getType(Uri uri) { + if (DEBUG) Log.d(TAG, "getType " + uri); + return SLICE_TYPE; + } + + @Override + public Bundle call(String method, String arg, Bundle extras) { + if (method.equals(METHOD_SLICE)) { + getContext().enforceCallingPermission(permission.BIND_SLICE, + "Slice binding requires the permission BIND_SLICE"); + Uri uri = extras.getParcelable(EXTRA_BIND_URI); + + Slice s = handleBindSlice(uri); + Bundle b = new Bundle(); + b.putParcelable(EXTRA_SLICE, s); + return b; + } + return super.call(method, arg, extras); + } + + 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); + latch.countDown(); + } + }); + try { + latch.await(); + return output[0]; + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } +} diff --git a/android/app/slice/SliceQuery.java b/android/app/slice/SliceQuery.java new file mode 100644 index 00000000..d1fe2c90 --- /dev/null +++ b/android/app/slice/SliceQuery.java @@ -0,0 +1,150 @@ +/* + * 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; + +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; +import java.util.Spliterators; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +/** + * A bunch of utilities for searching the contents of a slice. + * @hide + */ +public class SliceQuery { + private static final String TAG = "SliceQuery"; + + /** + * @hide + */ + public static SliceItem findNotContaining(SliceItem container, List<SliceItem> list) { + SliceItem ret = null; + while (ret == null && list.size() != 0) { + SliceItem remove = list.remove(0); + if (!contains(container, remove)) { + ret = remove; + } + } + return ret; + } + + /** + * @hide + */ + private static boolean contains(SliceItem container, SliceItem item) { + if (container == null || item == null) return false; + return stream(container).filter(s -> (s == item)).findAny().isPresent(); + } + + /** + * @hide + */ + public static List<SliceItem> findAll(SliceItem s, int type) { + return findAll(s, type, (String[]) null, null); + } + + /** + * @hide + */ + public static List<SliceItem> findAll(SliceItem s, int type, String hints, String nonHints) { + return findAll(s, type, new String[]{ hints }, new String[]{ nonHints }); + } + + /** + * @hide + */ + public static List<SliceItem> findAll(SliceItem s, int type, String[] hints, + String[] nonHints) { + return stream(s).filter(item -> (type == -1 || item.getType() == type) + && (item.hasHints(hints) && !item.hasAnyHints(nonHints))) + .collect(Collectors.toList()); + } + + /** + * @hide + */ + public static SliceItem find(Slice s, int type, String hints, String nonHints) { + return find(s, type, new String[]{ hints }, new String[]{ nonHints }); + } + + /** + * @hide + */ + public static SliceItem find(Slice s, int type) { + return find(s, type, (String[]) null, null); + } + + /** + * @hide + */ + public static SliceItem find(SliceItem s, int type) { + return find(s, type, (String[]) null, null); + } + + /** + * @hide + */ + public static SliceItem find(SliceItem s, int type, String hints, String nonHints) { + return find(s, type, new String[]{ hints }, new String[]{ nonHints }); + } + + /** + * @hide + */ + public static SliceItem find(Slice s, int type, String[] hints, String[] nonHints) { + List<String> h = s.getHints(); + return find(new SliceItem(s, SliceItem.TYPE_SLICE, h.toArray(new String[h.size()])), type, + hints, nonHints); + } + + /** + * @hide + */ + public static SliceItem find(SliceItem s, int type, String[] hints, String[] nonHints) { + return stream(s).filter(item -> (item.getType() == type || type == -1) + && (item.hasHints(hints) && !item.hasAnyHints(nonHints))).findFirst().orElse(null); + } + + /** + * @hide + */ + public static Stream<SliceItem> stream(SliceItem slice) { + Queue<SliceItem> items = new LinkedList(); + items.add(slice); + Iterator<SliceItem> iterator = new Iterator<SliceItem>() { + @Override + public boolean hasNext() { + return items.size() != 0; + } + + @Override + public SliceItem next() { + SliceItem item = items.poll(); + if (item.getType() == SliceItem.TYPE_SLICE + || item.getType() == SliceItem.TYPE_ACTION) { + items.addAll(item.getSlice().getItems()); + } + return item; + } + }; + return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, 0), false); + } +} diff --git a/android/app/slice/views/ActionRow.java b/android/app/slice/views/ActionRow.java new file mode 100644 index 00000000..c7d99f7f --- /dev/null +++ b/android/app/slice/views/ActionRow.java @@ -0,0 +1,201 @@ +/* + * 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.app.PendingIntent; +import android.app.PendingIntent.CanceledException; +import android.app.RemoteInput; +import android.app.slice.Slice; +import android.app.slice.SliceItem; +import android.app.slice.SliceQuery; +import android.content.Context; +import android.content.res.ColorStateList; +import android.graphics.Color; +import android.graphics.drawable.Icon; +import android.os.AsyncTask; +import android.util.TypedValue; +import android.view.View; +import android.view.ViewParent; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.ImageView.ScaleType; +import android.widget.LinearLayout; +import android.widget.TextView; + +/** + * @hide + */ +public class ActionRow extends FrameLayout { + + private static final int MAX_ACTIONS = 5; + private final int mSize; + private final int mIconPadding; + private final LinearLayout mActionsGroup; + private final boolean mFullActions; + private int mColor = Color.BLACK; + + public ActionRow(Context context, boolean fullActions) { + super(context); + mFullActions = fullActions; + mSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 48, + context.getResources().getDisplayMetrics()); + mIconPadding = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 12, + context.getResources().getDisplayMetrics()); + mActionsGroup = new LinearLayout(context); + mActionsGroup.setOrientation(LinearLayout.HORIZONTAL); + mActionsGroup.setLayoutParams( + new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); + addView(mActionsGroup); + } + + private void setColor(int color) { + mColor = color; + for (int i = 0; i < mActionsGroup.getChildCount(); i++) { + View view = mActionsGroup.getChildAt(i); + SliceItem item = (SliceItem) view.getTag(); + boolean tint = !item.hasHint(Slice.HINT_NO_TINT); + if (tint) { + ((ImageView) view).setImageTintList(ColorStateList.valueOf(mColor)); + } + } + } + + private ImageView addAction(Icon icon, boolean allowTint, SliceItem image) { + ImageView imageView = new ImageView(getContext()); + imageView.setPadding(mIconPadding, mIconPadding, mIconPadding, mIconPadding); + imageView.setScaleType(ScaleType.FIT_CENTER); + imageView.setImageIcon(icon); + if (allowTint) { + imageView.setImageTintList(ColorStateList.valueOf(mColor)); + } + imageView.setBackground(SliceViewUtil.getDrawable(getContext(), + android.R.attr.selectableItemBackground)); + imageView.setTag(image); + addAction(imageView); + return imageView; + } + + /** + * Set the actions and color for this action row. + */ + public void setActions(SliceItem actionRow, SliceItem defColor) { + removeAllViews(); + mActionsGroup.removeAllViews(); + addView(mActionsGroup); + + SliceItem color = SliceQuery.find(actionRow, SliceItem.TYPE_COLOR); + if (color == null) { + color = defColor; + } + if (color != null) { + setColor(color.getColor()); + } + SliceQuery.findAll(actionRow, SliceItem.TYPE_ACTION).forEach(action -> { + if (mActionsGroup.getChildCount() >= MAX_ACTIONS) { + return; + } + SliceItem image = SliceQuery.find(action, SliceItem.TYPE_IMAGE); + if (image == null) { + return; + } + boolean tint = !image.hasHint(Slice.HINT_NO_TINT); + SliceItem input = SliceQuery.find(action, SliceItem.TYPE_REMOTE_INPUT); + if (input != null && input.getRemoteInput().getAllowFreeFormInput()) { + addAction(image.getIcon(), tint, image).setOnClickListener( + v -> handleRemoteInputClick(v, action.getAction(), input.getRemoteInput())); + createRemoteInputView(mColor, getContext()); + } else { + addAction(image.getIcon(), tint, image).setOnClickListener(v -> AsyncTask.execute( + () -> { + try { + action.getAction().send(); + } catch (CanceledException e) { + e.printStackTrace(); + } + })); + } + }); + setVisibility(getChildCount() != 0 ? View.VISIBLE : View.GONE); + } + + private void addAction(View child) { + mActionsGroup.addView(child, new LinearLayout.LayoutParams(mSize, mSize, 1)); + } + + private void createRemoteInputView(int color, Context context) { + View riv = RemoteInputView.inflate(context, this); + riv.setVisibility(View.INVISIBLE); + addView(riv, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); + riv.setBackgroundColor(color); + } + + private boolean handleRemoteInputClick(View view, PendingIntent pendingIntent, + RemoteInput input) { + if (input == null) { + return false; + } + + ViewParent p = view.getParent().getParent(); + RemoteInputView riv = null; + while (p != null) { + if (p instanceof View) { + View pv = (View) p; + riv = findRemoteInputView(pv); + if (riv != null) { + break; + } + } + p = p.getParent(); + } + if (riv == null) { + return false; + } + + int width = view.getWidth(); + if (view instanceof TextView) { + // Center the reveal on the text which might be off-center from the TextView + TextView tv = (TextView) view; + if (tv.getLayout() != null) { + int innerWidth = (int) tv.getLayout().getLineWidth(0); + innerWidth += tv.getCompoundPaddingLeft() + tv.getCompoundPaddingRight(); + width = Math.min(width, innerWidth); + } + } + int cx = view.getLeft() + width / 2; + int cy = view.getTop() + view.getHeight() / 2; + int w = riv.getWidth(); + int h = riv.getHeight(); + int r = Math.max( + Math.max(cx + cy, cx + (h - cy)), + Math.max((w - cx) + cy, (w - cx) + (h - cy))); + + riv.setRevealParameters(cx, cy, r); + riv.setPendingIntent(pendingIntent); + riv.setRemoteInput(new RemoteInput[] { + input + }, input); + riv.focusAnimated(); + return true; + } + + private RemoteInputView findRemoteInputView(View v) { + if (v == null) { + return null; + } + return (RemoteInputView) v.findViewWithTag(RemoteInputView.VIEW_TAG); + } +} diff --git a/android/app/slice/views/GridView.java b/android/app/slice/views/GridView.java new file mode 100644 index 00000000..6f30c507 --- /dev/null +++ b/android/app/slice/views/GridView.java @@ -0,0 +1,186 @@ +/* + * 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 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.content.Context; +import android.graphics.Color; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.ImageView.ScaleType; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.android.internal.R; + +import java.util.ArrayList; +import java.util.List; + +/** + * @hide + */ +public class GridView extends LinearLayout implements SliceListView { + + private static final String TAG = "GridView"; + + private static final int MAX_IMAGES = 3; + private static final int MAX_ALL = 5; + private boolean mIsAllImages; + + public GridView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + if (mIsAllImages) { + int width = MeasureSpec.getSize(widthMeasureSpec); + int height = width / getChildCount(); + heightMeasureSpec = MeasureSpec.makeMeasureSpec(MeasureSpec.EXACTLY, + height); + getLayoutParams().height = height; + for (int i = 0; i < getChildCount(); i++) { + getChildAt(i).getLayoutParams().height = height; + } + } + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + + @Override + public void setSliceItem(SliceItem slice) { + mIsAllImages = true; + removeAllViews(); + int total = 1; + if (slice.getType() == SliceItem.TYPE_SLICE) { + List<SliceItem> items = slice.getSlice().getItems(); + total = items.size(); + for (int i = 0; i < total; i++) { + SliceItem item = items.get(i); + if (isFull()) { + continue; + } + if (!addItem(item)) { + mIsAllImages = false; + } + } + } else { + if (!isFull()) { + if (!addItem(slice)) { + mIsAllImages = false; + } + } + } + if (total > getChildCount() && mIsAllImages) { + addExtraCount(total - getChildCount()); + } + } + + private void addExtraCount(int numExtra) { + View last = getChildAt(getChildCount() - 1); + FrameLayout frame = new FrameLayout(getContext()); + frame.setLayoutParams(last.getLayoutParams()); + + removeView(last); + frame.addView(last, new LayoutParams(MATCH_PARENT, MATCH_PARENT)); + + TextView v = new TextView(getContext()); + v.setTextColor(Color.WHITE); + v.setBackgroundColor(0x4d000000); + v.setText(getResources().getString(R.string.slice_more_content, numExtra)); + v.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 18); + v.setGravity(Gravity.CENTER); + frame.addView(v, new LayoutParams(MATCH_PARENT, MATCH_PARENT)); + + addView(frame); + } + + private boolean isFull() { + return getChildCount() >= (mIsAllImages ? MAX_IMAGES : MAX_ALL); + } + + /** + * Returns true if this item is just an image. + */ + private boolean addItem(SliceItem item) { + if (item.getType() == SliceItem.TYPE_IMAGE) { + ImageView v = new ImageView(getContext()); + v.setImageIcon(item.getIcon()); + v.setScaleType(ScaleType.CENTER_CROP); + addView(v, new LayoutParams(0, MATCH_PARENT, 1)); + return true; + } else { + LinearLayout v = new LinearLayout(getContext()); + int s = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, + 12, getContext().getResources().getDisplayMetrics()); + v.setPadding(0, s, 0, 0); + v.setOrientation(LinearLayout.VERTICAL); + v.setGravity(Gravity.CENTER_HORIZONTAL); + // TODO: Unify sporadic inflates that happen throughout the code. + ArrayList<SliceItem> items = new ArrayList<>(); + if (item.getType() == SliceItem.TYPE_SLICE) { + items.addAll(item.getSlice().getItems()); + } + items.forEach(i -> { + Context context = getContext(); + switch (i.getType()) { + case SliceItem.TYPE_TEXT: + boolean title = false; + if ((item.hasAnyHints(new String[] { + Slice.HINT_LARGE, Slice.HINT_TITLE + }))) { + title = true; + } + TextView tv = (TextView) LayoutInflater.from(context).inflate( + title ? R.layout.slice_title : R.layout.slice_secondary_text, null); + tv.setText(i.getText()); + v.addView(tv); + break; + case SliceItem.TYPE_IMAGE: + ImageView iv = new ImageView(context); + iv.setImageIcon(i.getIcon()); + if (item.hasHint(Slice.HINT_LARGE)) { + iv.setLayoutParams(new LayoutParams(WRAP_CONTENT, WRAP_CONTENT)); + } else { + int size = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, + 48, context.getResources().getDisplayMetrics()); + iv.setLayoutParams(new LayoutParams(size, size)); + } + v.addView(iv); + break; + case SliceItem.TYPE_REMOTE_VIEW: + v.addView(i.getRemoteView().apply(context, v)); + break; + case SliceItem.TYPE_COLOR: + // TODO: Support color to tint stuff here. + break; + } + }); + addView(v, new LayoutParams(0, WRAP_CONTENT, 1)); + return false; + } + } +} diff --git a/android/app/slice/views/LargeSliceAdapter.java b/android/app/slice/views/LargeSliceAdapter.java new file mode 100644 index 00000000..6794ff98 --- /dev/null +++ b/android/app/slice/views/LargeSliceAdapter.java @@ -0,0 +1,224 @@ +/* + * 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.app.slice.Slice; +import android.app.slice.SliceItem; +import android.app.slice.SliceQuery; +import android.app.slice.views.LargeSliceAdapter.SliceViewHolder; +import android.content.Context; +import android.util.ArrayMap; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewGroup.LayoutParams; +import android.widget.FrameLayout; + +import com.android.internal.R; +import com.android.internal.widget.RecyclerView; +import com.android.internal.widget.RecyclerView.ViewHolder; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * @hide + */ +public class LargeSliceAdapter extends RecyclerView.Adapter<SliceViewHolder> { + + public static final int TYPE_DEFAULT = 1; + public static final int TYPE_HEADER = 2; + public static final int TYPE_GRID = 3; + public static final int TYPE_MESSAGE = 4; + public static final int TYPE_MESSAGE_LOCAL = 5; + public static final int TYPE_REMOTE_VIEWS = 6; + + private final IdGenerator mIdGen = new IdGenerator(); + private final Context mContext; + private List<SliceWrapper> mSlices = new ArrayList<>(); + private SliceItem mColor; + + public LargeSliceAdapter(Context context) { + mContext = context; + setHasStableIds(true); + } + + /** + * Set the {@link SliceItem}'s to be displayed in the adapter and the accent color. + */ + public void setSliceItems(List<SliceItem> slices, SliceItem color) { + mColor = color; + mIdGen.resetUsage(); + mSlices = slices.stream().map(s -> new SliceWrapper(s, mIdGen)) + .collect(Collectors.toList()); + notifyDataSetChanged(); + } + + @Override + public SliceViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View v = inflateforType(viewType); + v.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); + return new SliceViewHolder(v); + } + + @Override + public int getItemViewType(int position) { + return mSlices.get(position).mType; + } + + @Override + public long getItemId(int position) { + return mSlices.get(position).mId; + } + + @Override + public int getItemCount() { + return mSlices.size(); + } + + @Override + public void onBindViewHolder(SliceViewHolder holder, int position) { + SliceWrapper slice = mSlices.get(position); + if (holder.mSliceView != null) { + holder.mSliceView.setColor(mColor); + holder.mSliceView.setSliceItem(slice.mItem); + } else if (slice.mType == TYPE_REMOTE_VIEWS) { + FrameLayout frame = (FrameLayout) holder.itemView; + frame.removeAllViews(); + frame.addView(slice.mItem.getRemoteView().apply(mContext, frame)); + } + } + + private View inflateforType(int viewType) { + switch (viewType) { + case TYPE_REMOTE_VIEWS: + return new FrameLayout(mContext); + case TYPE_GRID: + return LayoutInflater.from(mContext).inflate(R.layout.slice_grid, null); + case TYPE_MESSAGE: + return LayoutInflater.from(mContext).inflate(R.layout.slice_message, null); + case TYPE_MESSAGE_LOCAL: + return LayoutInflater.from(mContext).inflate(R.layout.slice_message_local, null); + } + return new SmallTemplateView(mContext); + } + + protected static class SliceWrapper { + private final SliceItem mItem; + private final int mType; + private final long mId; + + public SliceWrapper(SliceItem item, IdGenerator idGen) { + mItem = item; + mType = getType(item); + mId = idGen.getId(item); + } + + public static int getType(SliceItem item) { + if (item.getType() == SliceItem.TYPE_REMOTE_VIEW) { + return TYPE_REMOTE_VIEWS; + } + if (item.hasHint(Slice.HINT_MESSAGE)) { + // TODO: Better way to determine me or not? Something more like Messaging style. + if (SliceQuery.find(item, -1, Slice.HINT_SOURCE, null) != null) { + return TYPE_MESSAGE; + } else { + return TYPE_MESSAGE_LOCAL; + } + } + if (item.hasHint(Slice.HINT_HORIZONTAL)) { + return TYPE_GRID; + } + return TYPE_DEFAULT; + } + } + + /** + * A {@link ViewHolder} for presenting slices in {@link LargeSliceAdapter}. + */ + public static class SliceViewHolder extends ViewHolder { + public final SliceListView mSliceView; + + public SliceViewHolder(View itemView) { + super(itemView); + mSliceView = itemView instanceof SliceListView ? (SliceListView) itemView : null; + } + } + + /** + * View slices being displayed in {@link LargeSliceAdapter}. + */ + public interface SliceListView { + /** + * Set the slice item for this view. + */ + void setSliceItem(SliceItem slice); + + /** + * Set the color for the items in this view. + */ + default void setColor(SliceItem color) { + + } + } + + private static class IdGenerator { + private long mNextLong = 0; + private final ArrayMap<String, Long> mCurrentIds = new ArrayMap<>(); + private final ArrayMap<String, Integer> mUsedIds = new ArrayMap<>(); + + public long getId(SliceItem item) { + String str = genString(item); + if (!mCurrentIds.containsKey(str)) { + mCurrentIds.put(str, mNextLong++); + } + long id = mCurrentIds.get(str); + int index = mUsedIds.getOrDefault(str, 0); + mUsedIds.put(str, index + 1); + return id + index * 10000; + } + + private String genString(SliceItem item) { + StringBuilder builder = new StringBuilder(); + SliceQuery.stream(item).forEach(i -> { + builder.append(i.getType()); + i.removeHint(Slice.HINT_SELECTED); + builder.append(i.getHints()); + switch (i.getType()) { + case SliceItem.TYPE_REMOTE_VIEW: + builder.append(i.getRemoteView()); + break; + case SliceItem.TYPE_IMAGE: + builder.append(i.getIcon()); + break; + case SliceItem.TYPE_TEXT: + builder.append(i.getText()); + break; + case SliceItem.TYPE_COLOR: + builder.append(i.getColor()); + break; + } + }); + return builder.toString(); + } + + public void resetUsage() { + mUsedIds.clear(); + } + } +} diff --git a/android/app/slice/views/LargeTemplateView.java b/android/app/slice/views/LargeTemplateView.java new file mode 100644 index 00000000..9e225162 --- /dev/null +++ b/android/app/slice/views/LargeTemplateView.java @@ -0,0 +1,115 @@ +/* + * 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 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.content.Context; +import android.util.TypedValue; + +import com.android.internal.widget.LinearLayoutManager; +import com.android.internal.widget.RecyclerView; + +import java.util.ArrayList; +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; + + public LargeTemplateView(Context context) { + super(context); + + mRecyclerView = new RecyclerView(getContext()); + mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext())); + 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, + getResources().getDisplayMetrics()); + } + + @Override + public String getMode() { + return SliceView.MODE_LARGE; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + mRecyclerView.getLayoutParams().height = WRAP_CONTENT; + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + if (mRecyclerView.getMeasuredHeight() > mMaxHeight + || mSlice.hasHint(Slice.HINT_PARTIAL)) { + mRecyclerView.getLayoutParams().height = mDefaultHeight; + } else { + mRecyclerView.getLayoutParams().height = mRecyclerView.getMeasuredHeight(); + } + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + + @Override + public void setSlice(Slice slice) { + SliceItem color = SliceQuery.find(slice, SliceItem.TYPE_COLOR); + mSlice = slice; + List<SliceItem> items = new ArrayList<>(); + boolean[] hasHeader = new boolean[1]; + if (slice.hasHint(Slice.HINT_LIST)) { + addList(slice, items); + } else { + slice.getItems().forEach(item -> { + if (item.hasHint(Slice.HINT_ACTIONS)) { + return; + } else if (item.getType() == SliceItem.TYPE_COLOR) { + return; + } else if (item.getType() == SliceItem.TYPE_SLICE + && item.hasHint(Slice.HINT_LIST)) { + addList(item.getSlice(), items); + } else if (item.hasHint(Slice.HINT_LIST_ITEM)) { + items.add(item); + } else if (!hasHeader[0]) { + hasHeader[0] = true; + items.add(0, item); + } else { + item.addHint(Slice.HINT_LIST_ITEM); + items.add(item); + } + }); + } + mAdapter.setSliceItems(items, color); + } + + private void addList(Slice slice, List<SliceItem> items) { + List<SliceItem> sliceItems = slice.getItems(); + sliceItems.forEach(i -> i.addHint(Slice.HINT_LIST_ITEM)); + items.addAll(sliceItems); + } +} diff --git a/android/app/slice/views/MessageView.java b/android/app/slice/views/MessageView.java new file mode 100644 index 00000000..77252bf2 --- /dev/null +++ b/android/app/slice/views/MessageView.java @@ -0,0 +1,77 @@ +/* + * 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.app.slice.Slice; +import android.app.slice.SliceItem; +import android.app.slice.SliceQuery; +import android.app.slice.views.LargeSliceAdapter.SliceListView; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.text.SpannableStringBuilder; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +/** + * @hide + */ +public class MessageView extends LinearLayout implements SliceListView { + + private TextView mDetails; + private ImageView mIcon; + + public MessageView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mDetails = findViewById(android.R.id.summary); + mIcon = findViewById(android.R.id.icon); + } + + @Override + public void setSliceItem(SliceItem slice) { + SliceItem source = SliceQuery.find(slice, SliceItem.TYPE_IMAGE, Slice.HINT_SOURCE, null); + if (source != null) { + final int iconSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, + 24, getContext().getResources().getDisplayMetrics()); + // TODO try and turn this into a drawable + Bitmap iconBm = Bitmap.createBitmap(iconSize, iconSize, Bitmap.Config.ARGB_8888); + Canvas iconCanvas = new Canvas(iconBm); + Drawable d = source.getIcon().loadDrawable(getContext()); + d.setBounds(0, 0, iconSize, iconSize); + d.draw(iconCanvas); + mIcon.setImageBitmap(SliceViewUtil.getCircularBitmap(iconBm)); + } + SpannableStringBuilder builder = new SpannableStringBuilder(); + SliceQuery.findAll(slice, SliceItem.TYPE_TEXT).forEach(text -> { + if (builder.length() != 0) { + builder.append('\n'); + } + builder.append(text.getText()); + }); + mDetails.setText(builder.toString()); + } + +} diff --git a/android/app/slice/views/RemoteInputView.java b/android/app/slice/views/RemoteInputView.java new file mode 100644 index 00000000..e53cb1ea --- /dev/null +++ b/android/app/slice/views/RemoteInputView.java @@ -0,0 +1,445 @@ +/* + * 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.animation.Animator; +import android.app.Notification; +import android.app.PendingIntent; +import android.app.RemoteInput; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ShortcutManager; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.AttributeSet; +import android.util.Log; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewAnimationUtils; +import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; +import android.view.inputmethod.CompletionInfo; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; +import android.view.inputmethod.InputMethodManager; +import android.widget.EditText; +import android.widget.ImageButton; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import android.widget.TextView; +import android.widget.Toast; + +import com.android.internal.R; + +/** + * Host for the remote input. + * + * @hide + */ +// TODO this should be unified with SystemUI RemoteInputView (b/67527720) +public class RemoteInputView extends LinearLayout implements View.OnClickListener, TextWatcher { + + private static final String TAG = "RemoteInput"; + + /** + * A marker object that let's us easily find views of this class. + */ + public static final Object VIEW_TAG = new Object(); + + private RemoteEditText mEditText; + private ImageButton mSendButton; + private ProgressBar mProgressBar; + private PendingIntent mPendingIntent; + private RemoteInput[] mRemoteInputs; + private RemoteInput mRemoteInput; + + private int mRevealCx; + private int mRevealCy; + private int mRevealR; + private boolean mResetting; + + public RemoteInputView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + mProgressBar = findViewById(R.id.remote_input_progress); + mSendButton = findViewById(R.id.remote_input_send); + mSendButton.setOnClickListener(this); + + mEditText = (RemoteEditText) getChildAt(0); + mEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() { + @Override + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + final boolean isSoftImeEvent = event == null + && (actionId == EditorInfo.IME_ACTION_DONE + || actionId == EditorInfo.IME_ACTION_NEXT + || actionId == EditorInfo.IME_ACTION_SEND); + final boolean isKeyboardEnterKey = event != null + && KeyEvent.isConfirmKey(event.getKeyCode()) + && event.getAction() == KeyEvent.ACTION_DOWN; + + if (isSoftImeEvent || isKeyboardEnterKey) { + if (mEditText.length() > 0) { + sendRemoteInput(); + } + // Consume action to prevent IME from closing. + return true; + } + return false; + } + }); + mEditText.addTextChangedListener(this); + mEditText.setInnerFocusable(false); + mEditText.mRemoteInputView = this; + } + + private void sendRemoteInput() { + Bundle results = new Bundle(); + results.putString(mRemoteInput.getResultKey(), mEditText.getText().toString()); + Intent fillInIntent = new Intent().addFlags(Intent.FLAG_RECEIVER_FOREGROUND); + RemoteInput.addResultsToIntent(mRemoteInputs, fillInIntent, + results); + + mEditText.setEnabled(false); + mSendButton.setVisibility(INVISIBLE); + mProgressBar.setVisibility(VISIBLE); + mEditText.mShowImeOnInputConnection = false; + + // Tell ShortcutManager that this package has been "activated". ShortcutManager + // will reset the throttling for this package. + // Strictly speaking, the intent receiver may be different from the intent creator, + // but that's an edge case, and also because we can't always know which package will receive + // an intent, so we just reset for the creator. + getContext().getSystemService(ShortcutManager.class).onApplicationActive( + mPendingIntent.getCreatorPackage(), + getContext().getUserId()); + + try { + mPendingIntent.send(mContext, 0, fillInIntent); + reset(); + } catch (PendingIntent.CanceledException e) { + Log.i(TAG, "Unable to send remote input result", e); + Toast.makeText(mContext, "Failure sending pending intent for inline reply :(", + Toast.LENGTH_SHORT).show(); + reset(); + } + } + + /** + * Creates a remote input view. + */ + public static RemoteInputView inflate(Context context, ViewGroup root) { + RemoteInputView v = (RemoteInputView) LayoutInflater.from(context).inflate( + R.layout.slice_remote_input, root, false); + v.setTag(VIEW_TAG); + return v; + } + + @Override + public void onClick(View v) { + if (v == mSendButton) { + sendRemoteInput(); + } + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + super.onTouchEvent(event); + + // We never want for a touch to escape to an outer view or one we covered. + return true; + } + + private void onDefocus() { + setVisibility(INVISIBLE); + } + + /** + * Set the pending intent for remote input. + */ + public void setPendingIntent(PendingIntent pendingIntent) { + mPendingIntent = pendingIntent; + } + + /** + * Set the remote inputs for this view. + */ + public void setRemoteInput(RemoteInput[] remoteInputs, RemoteInput remoteInput) { + mRemoteInputs = remoteInputs; + mRemoteInput = remoteInput; + mEditText.setHint(mRemoteInput.getLabel()); + } + + /** + * Focuses the remote input view. + */ + public void focusAnimated() { + if (getVisibility() != VISIBLE) { + Animator animator = ViewAnimationUtils.createCircularReveal( + this, mRevealCx, mRevealCy, 0, mRevealR); + animator.setDuration(200); + animator.start(); + } + focus(); + } + + private void focus() { + setVisibility(VISIBLE); + mEditText.setInnerFocusable(true); + mEditText.mShowImeOnInputConnection = true; + mEditText.setSelection(mEditText.getText().length()); + mEditText.requestFocus(); + updateSendButton(); + } + + private void reset() { + mResetting = true; + + mEditText.getText().clear(); + mEditText.setEnabled(true); + mSendButton.setVisibility(VISIBLE); + mProgressBar.setVisibility(INVISIBLE); + updateSendButton(); + onDefocus(); + + mResetting = false; + } + + @Override + public boolean onRequestSendAccessibilityEvent(View child, AccessibilityEvent event) { + if (mResetting && child == mEditText) { + // Suppress text events if it happens during resetting. Ideally this would be + // suppressed by the text view not being shown, but that doesn't work here because it + // needs to stay visible for the animation. + return false; + } + return super.onRequestSendAccessibilityEvent(child, event); + } + + private void updateSendButton() { + mSendButton.setEnabled(mEditText.getText().length() != 0); + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + + @Override + public void afterTextChanged(Editable s) { + updateSendButton(); + } + + /** + * Tries to find an action that matches the current pending intent of this view and updates its + * state to that of the found action + * + * @return true if a matching action was found, false otherwise + */ + public boolean updatePendingIntentFromActions(Notification.Action[] actions) { + if (mPendingIntent == null || actions == null) { + return false; + } + Intent current = mPendingIntent.getIntent(); + if (current == null) { + return false; + } + + for (Notification.Action a : actions) { + RemoteInput[] inputs = a.getRemoteInputs(); + if (a.actionIntent == null || inputs == null) { + continue; + } + Intent candidate = a.actionIntent.getIntent(); + if (!current.filterEquals(candidate)) { + continue; + } + + RemoteInput input = null; + for (RemoteInput i : inputs) { + if (i.getAllowFreeFormInput()) { + input = i; + } + } + if (input == null) { + continue; + } + setPendingIntent(a.actionIntent); + setRemoteInput(inputs, input); + return true; + } + return false; + } + + /** + * @hide + */ + public void setRevealParameters(int cx, int cy, int r) { + mRevealCx = cx; + mRevealCy = cy; + mRevealR = r; + } + + @Override + public void dispatchStartTemporaryDetach() { + super.dispatchStartTemporaryDetach(); + // Detach the EditText temporarily such that it doesn't get onDetachedFromWindow and + // won't lose IME focus. + detachViewFromParent(mEditText); + } + + @Override + public void dispatchFinishTemporaryDetach() { + if (isAttachedToWindow()) { + attachViewToParent(mEditText, 0, mEditText.getLayoutParams()); + } else { + removeDetachedView(mEditText, false /* animate */); + } + super.dispatchFinishTemporaryDetach(); + } + + /** + * An EditText that changes appearance based on whether it's focusable and becomes un-focusable + * whenever the user navigates away from it or it becomes invisible. + */ + public static class RemoteEditText extends EditText { + + private final Drawable mBackground; + private RemoteInputView mRemoteInputView; + boolean mShowImeOnInputConnection; + + public RemoteEditText(Context context, AttributeSet attrs) { + super(context, attrs); + mBackground = getBackground(); + } + + private void defocusIfNeeded(boolean animate) { + if (mRemoteInputView != null || isTemporarilyDetached()) { + if (isTemporarilyDetached()) { + // We might get reattached but then the other one of HUN / expanded might steal + // our focus, so we'll need to save our text here. + } + return; + } + if (isFocusable() && isEnabled()) { + setInnerFocusable(false); + if (mRemoteInputView != null) { + mRemoteInputView.onDefocus(); + } + mShowImeOnInputConnection = false; + } + } + + @Override + protected void onVisibilityChanged(View changedView, int visibility) { + super.onVisibilityChanged(changedView, visibility); + + if (!isShown()) { + defocusIfNeeded(false /* animate */); + } + } + + @Override + protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { + super.onFocusChanged(focused, direction, previouslyFocusedRect); + if (!focused) { + defocusIfNeeded(true /* animate */); + } + } + + @Override + public void getFocusedRect(Rect r) { + super.getFocusedRect(r); + r.top = mScrollY; + r.bottom = mScrollY + (mBottom - mTop); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK) { + // Eat the DOWN event here to prevent any default behavior. + return true; + } + return super.onKeyDown(keyCode, event); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK) { + defocusIfNeeded(true /* animate */); + return true; + } + return super.onKeyUp(keyCode, event); + } + + @Override + public InputConnection onCreateInputConnection(EditorInfo outAttrs) { + final InputConnection inputConnection = super.onCreateInputConnection(outAttrs); + + if (mShowImeOnInputConnection && inputConnection != null) { + final InputMethodManager imm = InputMethodManager.getInstance(); + if (imm != null) { + // onCreateInputConnection is called by InputMethodManager in the middle of + // setting up the connection to the IME; wait with requesting the IME until that + // work has completed. + post(new Runnable() { + @Override + public void run() { + imm.viewClicked(RemoteEditText.this); + imm.showSoftInput(RemoteEditText.this, 0); + } + }); + } + } + + return inputConnection; + } + + @Override + public void onCommitCompletion(CompletionInfo text) { + clearComposingText(); + setText(text.getText()); + setSelection(getText().length()); + } + + void setInnerFocusable(boolean focusable) { + setFocusableInTouchMode(focusable); + setFocusable(focusable); + setCursorVisible(focusable); + + if (focusable) { + requestFocus(); + setBackground(mBackground); + } else { + setBackground(null); + } + + } + } +} diff --git a/android/app/slice/views/ShortcutView.java b/android/app/slice/views/ShortcutView.java new file mode 100644 index 00000000..b6790c7d --- /dev/null +++ b/android/app/slice/views/ShortcutView.java @@ -0,0 +1,110 @@ +/* + * 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.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.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; + +/** + * @hide + */ +public class ShortcutView extends SliceModeView { + + private static final String TAG = "ShortcutView"; + + private PendingIntent mAction; + private Uri mUri; + private int mLargeIconSize; + private int mSmallIconSize; + + 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 textItem = sliceItem != null + ? SliceQuery.find(sliceItem, SliceItem.TYPE_TEXT) + : SliceQuery.find(slice, SliceItem.TYPE_TEXT); + SliceItem colorItem = sliceItem != null + ? SliceQuery.find(sliceItem, SliceItem.TYPE_COLOR) + : SliceQuery.find(slice, SliceItem.TYPE_COLOR); + if (colorItem == null) { + colorItem = SliceQuery.find(slice, SliceItem.TYPE_COLOR); + } + // TODO: pick better default colour + final int color = colorItem != null ? colorItem.getColor() : Color.GRAY; + ShapeDrawable circle = new ShapeDrawable(new OvalShape()); + circle.setTint(color); + setBackground(circle); + if (iconItem != null) { + final boolean isLarge = iconItem.hasHint(Slice.HINT_LARGE); + final int iconSize = isLarge ? mLargeIconSize : mSmallIconSize; + SliceViewUtil.createCircledIcon(getContext(), color, iconSize, iconItem.getIcon(), + isLarge, this /* parent */); + mAction = sliceItem != null ? sliceItem.getAction() + : null; + mUri = slice.getUri(); + setClickable(true); + } else { + setClickable(false); + } + } + + @Override + public String getMode() { + return SliceView.MODE_SHORTCUT; + } + + @Override + public boolean performClick() { + if (!callOnClick()) { + try { + if (mAction != null) { + mAction.send(); + } else { + Intent intent = new Intent(Intent.ACTION_VIEW).setData(mUri); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + getContext().startActivity(intent); + } + } catch (CanceledException e) { + e.printStackTrace(); + } + } + return true; + } +} diff --git a/android/app/slice/views/SliceView.java b/android/app/slice/views/SliceView.java new file mode 100644 index 00000000..32484fca --- /dev/null +++ b/android/app/slice/views/SliceView.java @@ -0,0 +1,251 @@ +/* + * 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/SliceViewUtil.java b/android/app/slice/views/SliceViewUtil.java new file mode 100644 index 00000000..19e8e7c9 --- /dev/null +++ b/android/app/slice/views/SliceViewUtil.java @@ -0,0 +1,182 @@ +/* + * 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.ColorInt; +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.PorterDuff.Mode; +import android.graphics.PorterDuffXfermode; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.Icon; +import android.view.Gravity; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.ImageView; + +/** + * A bunch of utilities for slice UI. + * + * @hide + */ +public class SliceViewUtil { + + /** + * @hide + */ + @ColorInt + public static int getColorAccent(Context context) { + return getColorAttr(context, android.R.attr.colorAccent); + } + + /** + * @hide + */ + @ColorInt + public static int getColorError(Context context) { + return getColorAttr(context, android.R.attr.colorError); + } + + /** + * @hide + */ + @ColorInt + public static int getDefaultColor(Context context, int resId) { + final ColorStateList list = context.getResources().getColorStateList(resId, + context.getTheme()); + + return list.getDefaultColor(); + } + + /** + * @hide + */ + @ColorInt + public static int getDisabled(Context context, int inputColor) { + return applyAlphaAttr(context, android.R.attr.disabledAlpha, inputColor); + } + + /** + * @hide + */ + @ColorInt + public static int applyAlphaAttr(Context context, int attr, int inputColor) { + TypedArray ta = context.obtainStyledAttributes(new int[] { + attr + }); + float alpha = ta.getFloat(0, 0); + ta.recycle(); + return applyAlpha(alpha, inputColor); + } + + /** + * @hide + */ + @ColorInt + public static int applyAlpha(float alpha, int inputColor) { + alpha *= Color.alpha(inputColor); + return Color.argb((int) (alpha), Color.red(inputColor), Color.green(inputColor), + Color.blue(inputColor)); + } + + /** + * @hide + */ + @ColorInt + public static int getColorAttr(Context context, int attr) { + TypedArray ta = context.obtainStyledAttributes(new int[] { + attr + }); + @ColorInt + int colorAccent = ta.getColor(0, 0); + ta.recycle(); + return colorAccent; + } + + /** + * @hide + */ + public static int getThemeAttr(Context context, int attr) { + TypedArray ta = context.obtainStyledAttributes(new int[] { + attr + }); + int theme = ta.getResourceId(0, 0); + ta.recycle(); + return theme; + } + + /** + * @hide + */ + public static Drawable getDrawable(Context context, int attr) { + TypedArray ta = context.obtainStyledAttributes(new int[] { + attr + }); + Drawable drawable = ta.getDrawable(0); + ta.recycle(); + return drawable; + } + + /** + * @hide + */ + public static void createCircledIcon(Context context, int color, int iconSize, Icon icon, + boolean isLarge, ViewGroup parent) { + ImageView v = new ImageView(context); + v.setImageIcon(icon); + parent.addView(v); + FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) v.getLayoutParams(); + if (isLarge) { + // XXX better way to convert from icon -> bitmap or crop an icon (?) + Bitmap iconBm = Bitmap.createBitmap(iconSize, iconSize, Bitmap.Config.ARGB_8888); + Canvas iconCanvas = new Canvas(iconBm); + v.layout(0, 0, iconSize, iconSize); + v.draw(iconCanvas); + v.setImageBitmap(getCircularBitmap(iconBm)); + } else { + v.setColorFilter(Color.WHITE); + } + lp.width = iconSize; + lp.height = iconSize; + lp.gravity = Gravity.CENTER; + } + + /** + * @hide + */ + public static Bitmap getCircularBitmap(Bitmap bitmap) { + Bitmap output = Bitmap.createBitmap(bitmap.getWidth(), + bitmap.getHeight(), Config.ARGB_8888); + Canvas canvas = new Canvas(output); + final Paint paint = new Paint(); + final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()); + paint.setAntiAlias(true); + canvas.drawARGB(0, 0, 0, 0); + canvas.drawCircle(bitmap.getWidth() / 2, bitmap.getHeight() / 2, + bitmap.getWidth() / 2, paint); + paint.setXfermode(new PorterDuffXfermode(Mode.SRC_IN)); + canvas.drawBitmap(bitmap, rect, rect, paint); + return output; + } +} diff --git a/android/app/slice/views/SmallTemplateView.java b/android/app/slice/views/SmallTemplateView.java new file mode 100644 index 00000000..42b2d213 --- /dev/null +++ b/android/app/slice/views/SmallTemplateView.java @@ -0,0 +1,211 @@ +/* + * 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.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.content.Context; +import android.os.AsyncTask; +import android.view.View; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.android.internal.R; + +import java.text.Format; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * Small template is also used to construct list items for use with {@link LargeTemplateView}. + * + * @hide + */ +public class SmallTemplateView extends SliceModeView implements SliceListView { + + private static final String TAG = "SmallTemplateView"; + + private int mIconSize; + private int mPadding; + + private LinearLayout mStartContainer; + private TextView mTitleText; + private TextView mSecondaryText; + private LinearLayout mEndContainer; + + public SmallTemplateView(Context context) { + super(context); + inflate(context, R.layout.slice_small_template, this); + mIconSize = getContext().getResources().getDimensionPixelSize(R.dimen.slice_icon_size); + mPadding = getContext().getResources().getDimensionPixelSize(R.dimen.slice_padding); + + mStartContainer = (LinearLayout) findViewById(android.R.id.icon_frame); + mTitleText = (TextView) findViewById(android.R.id.title); + mSecondaryText = (TextView) findViewById(android.R.id.summary); + mEndContainer = (LinearLayout) findViewById(android.R.id.widget_frame); + } + + @Override + public String getMode() { + return SliceView.MODE_SMALL; + } + + @Override + public void setSliceItem(SliceItem slice) { + resetViews(); + SliceItem colorItem = SliceQuery.find(slice, SliceItem.TYPE_COLOR); + int color = colorItem != null ? colorItem.getColor() : -1; + + // Look for any title elements + List<SliceItem> titleItems = SliceQuery.findAll(slice, -1, Slice.HINT_TITLE, + null); + boolean hasTitleText = false; + boolean hasTitleItem = false; + for (int i = 0; i < titleItems.size(); i++) { + SliceItem item = titleItems.get(i); + if (!hasTitleItem) { + // icon, action icon, or timestamp + if (item.getType() == SliceItem.TYPE_ACTION) { + hasTitleItem = addIcon(item, color, mStartContainer); + } else if (item.getType() == SliceItem.TYPE_IMAGE) { + addIcon(item, color, mStartContainer); + hasTitleItem = true; + } else if (item.getType() == SliceItem.TYPE_TIMESTAMP) { + TextView tv = new TextView(getContext()); + tv.setText(convertTimeToString(item.getTimestamp())); + hasTitleItem = true; + } + } + if (!hasTitleText && item.getType() == SliceItem.TYPE_TEXT) { + mTitleText.setText(item.getText()); + hasTitleText = true; + } + if (hasTitleText && hasTitleItem) { + break; + } + } + mTitleText.setVisibility(hasTitleText ? View.VISIBLE : View.GONE); + mStartContainer.setVisibility(hasTitleItem ? View.VISIBLE : View.GONE); + + if (slice.getType() != SliceItem.TYPE_SLICE) { + return; + } + + // Deal with remaining items + int itemCount = 0; + boolean hasSummary = false; + ArrayList<SliceItem> sliceItems = new ArrayList<SliceItem>( + slice.getSlice().getItems()); + for (int i = 0; i < sliceItems.size(); i++) { + SliceItem item = sliceItems.get(i); + if (!hasSummary && item.getType() == SliceItem.TYPE_TEXT + && !item.hasHint(Slice.HINT_TITLE)) { + // TODO -- Should combine all text items? + mSecondaryText.setText(item.getText()); + hasSummary = true; + } + if (itemCount <= 3) { + if (item.getType() == SliceItem.TYPE_ACTION) { + if (addIcon(item, color, mEndContainer)) { + itemCount++; + } + } else if (item.getType() == SliceItem.TYPE_IMAGE) { + addIcon(item, color, mEndContainer); + itemCount++; + } else if (item.getType() == SliceItem.TYPE_TIMESTAMP) { + TextView tv = new TextView(getContext()); + tv.setText(convertTimeToString(item.getTimestamp())); + mEndContainer.addView(tv); + itemCount++; + } else if (item.getType() == SliceItem.TYPE_SLICE) { + List<SliceItem> subItems = item.getSlice().getItems(); + for (int j = 0; j < subItems.size(); j++) { + sliceItems.add(subItems.get(j)); + } + } + } + } + } + + @Override + public void setSlice(Slice slice) { + setSliceItem(new SliceItem(slice, SliceItem.TYPE_SLICE, + slice.getHints().toArray(new String[slice.getHints().size()]))); + } + + /** + * @return Whether an icon was added. + */ + private boolean addIcon(SliceItem sliceItem, int color, LinearLayout container) { + SliceItem image = null; + SliceItem action = null; + if (sliceItem.getType() == SliceItem.TYPE_ACTION) { + image = SliceQuery.find(sliceItem.getSlice(), SliceItem.TYPE_IMAGE); + action = sliceItem; + } else if (sliceItem.getType() == SliceItem.TYPE_IMAGE) { + image = sliceItem; + } + if (image != null) { + ImageView iv = new ImageView(getContext()); + iv.setImageIcon(image.getIcon()); + if (action != null) { + final SliceItem sliceAction = action; + iv.setOnClickListener(v -> AsyncTask.execute( + () -> { + try { + sliceAction.getAction().send(); + } catch (CanceledException e) { + e.printStackTrace(); + } + })); + iv.setBackground(SliceViewUtil.getDrawable(getContext(), + android.R.attr.selectableItemBackground)); + } + if (color != -1 && !sliceItem.hasHint(Slice.HINT_NO_TINT)) { + iv.setColorFilter(color); + } + container.addView(iv); + LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) iv.getLayoutParams(); + lp.width = mIconSize; + lp.height = mIconSize; + lp.setMarginStart(mPadding); + return true; + } + return false; + } + + private String convertTimeToString(long time) { + // TODO -- figure out what format(s) we support + Date date = new Date(time); + Format format = new SimpleDateFormat("MM dd yyyy HH:mm:ss"); + return format.format(date); + } + + private void resetViews() { + mStartContainer.removeAllViews(); + mEndContainer.removeAllViews(); + mTitleText.setText(null); + mSecondaryText.setText(null); + } +} diff --git a/android/app/usage/UsageStatsManager.java b/android/app/usage/UsageStatsManager.java index 051dccbd..fd579fce 100644 --- a/android/app/usage/UsageStatsManager.java +++ b/android/app/usage/UsageStatsManager.java @@ -48,10 +48,10 @@ import java.util.Map; * </pre> * A request for data in the middle of a time interval will include that interval. * <p/> - * <b>NOTE:</b> This API requires the permission android.permission.PACKAGE_USAGE_STATS, which - * is a system-level permission and will not be granted to third-party apps. However, declaring - * the permission implies intention to use the API and the user of the device can grant permission - * through the Settings application. + * <b>NOTE:</b> This API requires the permission android.permission.PACKAGE_USAGE_STATS. + * However, declaring the permission implies intention to use the API and the user of the device + * still needs to grant permission through the Settings application. + * See {@link android.provider.Settings#ACTION_USAGE_ACCESS_SETTINGS} */ @SystemService(Context.USAGE_STATS_SERVICE) public final class UsageStatsManager { @@ -122,7 +122,7 @@ public final class UsageStatsManager { * @param intervalType The time interval by which the stats are aggregated. * @param beginTime The inclusive beginning of the range of stats to include in the results. * @param endTime The exclusive end of the range of stats to include in the results. - * @return A list of {@link UsageStats} or null if none are available. + * @return A list of {@link UsageStats} * * @see #INTERVAL_DAILY * @see #INTERVAL_WEEKLY @@ -139,7 +139,7 @@ public final class UsageStatsManager { return slice.getList(); } } catch (RemoteException e) { - // fallthrough and return null. + // fallthrough and return the empty list. } return Collections.emptyList(); } @@ -152,7 +152,7 @@ public final class UsageStatsManager { * @param intervalType The time interval by which the stats are aggregated. * @param beginTime The inclusive beginning of the range of stats to include in the results. * @param endTime The exclusive end of the range of stats to include in the results. - * @return A list of {@link ConfigurationStats} or null if none are available. + * @return A list of {@link ConfigurationStats} */ public List<ConfigurationStats> queryConfigurations(int intervalType, long beginTime, long endTime) { @@ -185,7 +185,7 @@ public final class UsageStatsManager { return iter; } } catch (RemoteException e) { - // fallthrough and return null + // fallthrough and return empty result. } return sEmptyResults; } @@ -197,8 +197,7 @@ public final class UsageStatsManager { * * @param beginTime The inclusive beginning of the range of stats to include in the results. * @param endTime The exclusive end of the range of stats to include in the results. - * @return A {@link java.util.Map} keyed by package name, or null if no stats are - * available. + * @return A {@link java.util.Map} keyed by package name */ public Map<String, UsageStats> queryAndAggregateUsageStats(long beginTime, long endTime) { List<UsageStats> stats = queryUsageStats(INTERVAL_BEST, beginTime, endTime); |