diff options
author | 2017-10-09 19:28:16 +0000 | |
---|---|---|
committer | 2017-10-09 19:28:16 +0000 | |
commit | 7827bc93344f205d40e84e67fbaf48e39a4587f3 (patch) | |
tree | c368f00840715f9e0580614ef5efd90ed14a7295 | |
parent | 071691f756e4a72a74d415e2b052011e291628be (diff) | |
parent | 4575aa52d7736ab51a4f72f5852b98d357783839 (diff) |
Merge "First version of SliceView (hidden for now)"
27 files changed, 2483 insertions, 4 deletions
diff --git a/core/java/android/content/ContentProvider.java b/core/java/android/content/ContentProvider.java index cdeaea3ebcae..5b2bf456b4e8 100644 --- a/core/java/android/content/ContentProvider.java +++ b/core/java/android/content/ContentProvider.java @@ -2099,7 +2099,8 @@ public abstract class ContentProvider implements ComponentCallbacks2 { public static Uri maybeAddUserId(Uri uri, int userId) { if (uri == null) return null; if (userId != UserHandle.USER_CURRENT - && ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) { + && (ContentResolver.SCHEME_CONTENT.equals(uri.getScheme()) + || ContentResolver.SCHEME_SLICE.equals(uri.getScheme()))) { if (!uriHasUserId(uri)) { //We don't add the user Id if there's already one Uri.Builder builder = uri.buildUpon(); diff --git a/core/java/android/slice/Slice.java b/core/java/android/slice/Slice.java index bb810e634e4f..576865480e04 100644 --- a/core/java/android/slice/Slice.java +++ b/core/java/android/slice/Slice.java @@ -35,6 +35,8 @@ import android.os.Parcel; import android.os.Parcelable; import android.widget.RemoteViews; +import com.android.internal.util.ArrayUtils; + import java.util.ArrayList; import java.util.Arrays; @@ -136,7 +138,7 @@ public final class Slice implements Parcelable { } /** - * @return The Uri that this slice represents. + * @return The Uri that this Slice represents. */ public Uri getUri() { return mUri; @@ -191,6 +193,13 @@ public final class Slice implements Parcelable { } /** + * @hide + */ + public boolean hasHint(@SliceHint String hint) { + return ArrayUtils.contains(mHints, hint); + } + + /** * A Builder used to construct {@link Slice}s */ public static class Builder { @@ -308,4 +317,31 @@ public final class Slice implements Parcelable { return new Slice[size]; } }; + + /** + * @hide + * @return A string representation of this slice. + */ + public String getString() { + return getString(""); + } + + private String getString(String indent) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < mItems.length; i++) { + sb.append(indent); + if (mItems[i].getType() == TYPE_SLICE) { + sb.append("slice:\n"); + sb.append(mItems[i].getSlice().getString(indent + " ")); + } else if (mItems[i].getType() == 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(); + } } diff --git a/core/java/android/slice/SliceItem.java b/core/java/android/slice/SliceItem.java index 16f7dc669f32..2827ab9d994c 100644 --- a/core/java/android/slice/SliceItem.java +++ b/core/java/android/slice/SliceItem.java @@ -132,6 +132,13 @@ public final class SliceItem implements Parcelable { 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; } @@ -230,7 +237,7 @@ public final class SliceItem implements Parcelable { public boolean hasHints(@SliceHint String[] hints) { if (hints == null) return true; for (String hint : hints) { - if (!ArrayUtils.contains(mHints, hint)) { + if (!TextUtils.isEmpty(hint) && !ArrayUtils.contains(mHints, hint)) { return false; } } @@ -241,7 +248,7 @@ public final class SliceItem implements Parcelable { * @hide */ public boolean hasAnyHints(@SliceHint String[] hints) { - if (hints == null) return true; + if (hints == null) return false; for (String hint : hints) { if (ArrayUtils.contains(mHints, hint)) { return true; @@ -309,4 +316,29 @@ public final class SliceItem implements Parcelable { 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/core/java/android/slice/SliceQuery.java b/core/java/android/slice/SliceQuery.java index edac0ccaf1dd..d99b26a507e4 100644 --- a/core/java/android/slice/SliceQuery.java +++ b/core/java/android/slice/SliceQuery.java @@ -61,6 +61,13 @@ public class SliceQuery { /** * @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 }); } @@ -85,6 +92,13 @@ public class SliceQuery { /** * @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); } diff --git a/core/java/android/slice/views/ActionRow.java b/core/java/android/slice/views/ActionRow.java new file mode 100644 index 000000000000..93e9c0352580 --- /dev/null +++ b/core/java/android/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.slice.views; + +import android.app.PendingIntent; +import android.app.PendingIntent.CanceledException; +import android.app.RemoteInput; +import android.content.Context; +import android.content.res.ColorStateList; +import android.graphics.Color; +import android.graphics.drawable.Icon; +import android.os.AsyncTask; +import android.slice.Slice; +import android.slice.SliceItem; +import android.slice.SliceQuery; +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/core/java/android/slice/views/GridView.java b/core/java/android/slice/views/GridView.java new file mode 100644 index 000000000000..18a90f7d1405 --- /dev/null +++ b/core/java/android/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.slice.views; + +import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; +import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; + +import android.content.Context; +import android.graphics.Color; +import android.slice.Slice; +import android.slice.SliceItem; +import android.slice.views.LargeSliceAdapter.SliceListView; +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.Arrays; + +/** + * @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) { + SliceItem[] items = slice.getSlice().getItems(); + total = items.length; + for (int i = 0; i < total; i++) { + SliceItem item = items[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(Arrays.asList(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/core/java/android/slice/views/LargeSliceAdapter.java b/core/java/android/slice/views/LargeSliceAdapter.java new file mode 100644 index 000000000000..e77a1b2af745 --- /dev/null +++ b/core/java/android/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.slice.views; + +import android.content.Context; +import android.slice.Slice; +import android.slice.SliceItem; +import android.slice.SliceQuery; +import android.slice.views.LargeSliceAdapter.SliceViewHolder; +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/core/java/android/slice/views/LargeTemplateView.java b/core/java/android/slice/views/LargeTemplateView.java new file mode 100644 index 000000000000..d53e8fcb39e7 --- /dev/null +++ b/core/java/android/slice/views/LargeTemplateView.java @@ -0,0 +1,116 @@ +/* + * 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.slice.views; + +import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; + +import android.content.Context; +import android.slice.Slice; +import android.slice.SliceItem; +import android.slice.SliceQuery; +import android.slice.views.SliceView.SliceModeView; +import android.util.TypedValue; + +import com.android.internal.widget.LinearLayoutManager; +import com.android.internal.widget.RecyclerView; + +import java.util.ArrayList; +import java.util.Arrays; +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 { + Arrays.asList(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 = Arrays.asList(slice.getItems()); + sliceItems.forEach(i -> i.addHint(Slice.HINT_LIST_ITEM)); + items.addAll(sliceItems); + } +} diff --git a/core/java/android/slice/views/MessageView.java b/core/java/android/slice/views/MessageView.java new file mode 100644 index 000000000000..7b03e0bd92c7 --- /dev/null +++ b/core/java/android/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.slice.views; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.slice.Slice; +import android.slice.SliceItem; +import android.slice.SliceQuery; +import android.slice.views.LargeSliceAdapter.SliceListView; +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/core/java/android/slice/views/RemoteInputView.java b/core/java/android/slice/views/RemoteInputView.java new file mode 100644 index 000000000000..a29bb5c0e608 --- /dev/null +++ b/core/java/android/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.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/core/java/android/slice/views/ShortcutView.java b/core/java/android/slice/views/ShortcutView.java new file mode 100644 index 000000000000..8fe2f1ac9e6f --- /dev/null +++ b/core/java/android/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.slice.views; + +import android.app.PendingIntent; +import android.app.PendingIntent.CanceledException; +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.slice.Slice; +import android.slice.SliceItem; +import android.slice.SliceQuery; +import android.slice.views.SliceView.SliceModeView; +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/core/java/android/slice/views/SliceView.java b/core/java/android/slice/views/SliceView.java new file mode 100644 index 000000000000..f37924816bf2 --- /dev/null +++ b/core/java/android/slice/views/SliceView.java @@ -0,0 +1,249 @@ +/* + * 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.slice.views; + +import android.annotation.StringDef; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.graphics.drawable.ColorDrawable; +import android.net.Uri; +import android.slice.Slice; +import android.slice.SliceItem; +import android.slice.SliceQuery; +import android.util.Log; +import android.view.MotionEvent; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.LinearLayout; + +/** + * 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 = mContext.getContentResolver().bindSlice(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); + 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.length > 1 || (items.length != 0 && items[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_SLICE.equals(sliceUri.getScheme())) { + throw new RuntimeException("Invalid uri " + sliceUri); + } + if (sliceUri.getPathSegments().size() == 0) { + throw new RuntimeException("Invalid uri " + sliceUri); + } + } +} diff --git a/core/java/android/slice/views/SliceViewUtil.java b/core/java/android/slice/views/SliceViewUtil.java new file mode 100644 index 000000000000..1b5a6d1e088b --- /dev/null +++ b/core/java/android/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.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/core/java/android/slice/views/SmallTemplateView.java b/core/java/android/slice/views/SmallTemplateView.java new file mode 100644 index 000000000000..b0b181ed0169 --- /dev/null +++ b/core/java/android/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.slice.views; + +import android.app.PendingIntent.CanceledException; +import android.content.Context; +import android.os.AsyncTask; +import android.slice.Slice; +import android.slice.SliceItem; +import android.slice.SliceQuery; +import android.slice.views.LargeSliceAdapter.SliceListView; +import android.slice.views.SliceView.SliceModeView; +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.Arrays; +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>( + Arrays.asList(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) { + SliceItem[] subItems = item.getSlice().getItems(); + for (int j = 0; j < subItems.length; j++) { + sliceItems.add(subItems[j]); + } + } + } + } + } + + @Override + public void setSlice(Slice slice) { + setSliceItem(new SliceItem(slice, SliceItem.TYPE_SLICE, slice.getHints())); + } + + /** + * @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/core/res/res/drawable/ic_slice_send.xml b/core/res/res/drawable/ic_slice_send.xml new file mode 100644 index 000000000000..b8b6954a7aa3 --- /dev/null +++ b/core/res/res/drawable/ic_slice_send.xml @@ -0,0 +1,24 @@ +<!-- 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:autoMirrored="true" + android:width="24.0dp" + android:height="24.0dp" + android:viewportWidth="48.0" + android:viewportHeight="48.0"> + <path + android:fillColor="#FF000000" + android:pathData="M4.02,42.0L46.0,24.0 4.02,6.0 4.0,20.0l30.0,4.0 -30.0,4.0z"/> +</vector>
\ No newline at end of file diff --git a/core/res/res/drawable/slice_remote_input_bg.xml b/core/res/res/drawable/slice_remote_input_bg.xml new file mode 100644 index 000000000000..312067957f1f --- /dev/null +++ b/core/res/res/drawable/slice_remote_input_bg.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2016 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. +--> +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <solid android:color="#ff6c6c6c" /> + <corners + android:bottomRightRadius="16dp" + android:bottomLeftRadius="16dp"/> +</shape> diff --git a/core/res/res/drawable/slice_ripple_drawable.xml b/core/res/res/drawable/slice_ripple_drawable.xml new file mode 100644 index 000000000000..5ba1f07e5033 --- /dev/null +++ b/core/res/res/drawable/slice_ripple_drawable.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2014 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 + --> + +<ripple xmlns:android="http://schemas.android.com/apk/res/android" + android:color="?android:attr/colorControlHighlight" />
\ No newline at end of file diff --git a/core/res/res/layout/slice_grid.xml b/core/res/res/layout/slice_grid.xml new file mode 100644 index 000000000000..70df76b0ec60 --- /dev/null +++ b/core/res/res/layout/slice_grid.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> +<android.slice.views.GridView + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="?android:attr/listPreferredItemHeightSmall" + android:gravity="center_vertical" + android:background="?android:attr/activatedBackgroundIndicator" + android:clipToPadding="false"> +</android.slice.views.GridView> diff --git a/core/res/res/layout/slice_message.xml b/core/res/res/layout/slice_message.xml new file mode 100644 index 000000000000..a3279b652c84 --- /dev/null +++ b/core/res/res/layout/slice_message.xml @@ -0,0 +1,51 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> +<android.slice.views.MessageView + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingTop="12dp" + android:paddingStart="?android:attr/listPreferredItemPaddingStart" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" + android:background="?android:attr/activatedBackgroundIndicator" + android:clipToPadding="false"> + + <LinearLayout + android:id="@android:id/icon_frame" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="-4dp" + android:gravity="start|center_vertical" + android:orientation="horizontal" + android:paddingEnd="12dp" + android:paddingTop="4dp" + android:paddingBottom="4dp"> + <!-- TODO: Support text source --> + <ImageView + android:id="@android:id/icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:maxWidth="48dp" + android:maxHeight="48dp" /> + </LinearLayout> + + <TextView android:id="@android:id/summary" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignStart="@android:id/title" + android:textAppearance="?android:attr/textAppearanceListItem" + android:maxLines="10" /> +</android.slice.views.MessageView> diff --git a/core/res/res/layout/slice_message_local.xml b/core/res/res/layout/slice_message_local.xml new file mode 100644 index 000000000000..d4180f35250b --- /dev/null +++ b/core/res/res/layout/slice_message_local.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> +<android.slice.views.MessageView + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center_vertical|end" + android:paddingTop="12dp" + android:paddingStart="?android:attr/listPreferredItemPaddingStart" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" + android:background="?android:attr/activatedBackgroundIndicator" + android:clipToPadding="false"> + + <TextView android:id="@android:id/summary" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignStart="@android:id/title" + android:layout_gravity="end" + android:gravity="end" + android:padding="8dp" + android:textAppearance="?android:attr/textAppearanceListItem" + android:background="#ffeeeeee" + android:maxLines="10" /> + +</android.slice.views.MessageView> diff --git a/core/res/res/layout/slice_remote_input.xml b/core/res/res/layout/slice_remote_input.xml new file mode 100644 index 000000000000..dc570c43ef99 --- /dev/null +++ b/core/res/res/layout/slice_remote_input.xml @@ -0,0 +1,76 @@ +<?xml version="1.0" encoding="utf-8"?> + +<!-- 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. +--> +<!-- LinearLayout --> +<android.slice.views.RemoteInputView + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/remote_input" + android:background="@drawable/slice_remote_input_bg" + android:layout_height="match_parent" + android:layout_width="match_parent"> + + <view class="com.android.internal.slice.view.RemoteInputView$RemoteEditText" + android:id="@+id/remote_input_text" + android:layout_height="match_parent" + android:layout_width="0dp" + android:layout_weight="1" + android:paddingTop="2dp" + android:paddingBottom="4dp" + android:paddingStart="16dp" + android:paddingEnd="12dp" + android:gravity="start|center_vertical" + android:textAppearance="?android:attr/textAppearance" + android:textColor="#FFFFFFFF" + android:textColorHint="#99ffffff" + android:textSize="16sp" + android:background="@null" + android:singleLine="true" + android:ellipsize="start" + android:inputType="textShortMessage|textAutoCorrect|textCapSentences" + android:imeOptions="actionSend|flagNoExtractUi|flagNoFullscreen" /> + + <FrameLayout + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:layout_gravity="center_vertical"> + + <ImageButton + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:paddingStart="12dp" + android:paddingEnd="24dp" + android:paddingTop="16dp" + android:paddingBottom="16dp" + android:id="@+id/remote_input_send" + android:src="@drawable/ic_slice_send" + android:tint="#FFFFFF" + android:tintMode="src_in" + android:background="@drawable/slice_ripple_drawable" /> + + <ProgressBar + android:id="@+id/remote_input_progress" + android:layout_width="24dp" + android:layout_height="24dp" + android:layout_marginEnd="6dp" + android:layout_gravity="center" + android:visibility="invisible" + android:indeterminate="true" + style="?android:attr/progressBarStyleSmall" /> + + </FrameLayout> + +</android.slice.views.RemoteInputView>
\ No newline at end of file diff --git a/core/res/res/layout/slice_secondary_text.xml b/core/res/res/layout/slice_secondary_text.xml new file mode 100644 index 000000000000..80a15747f528 --- /dev/null +++ b/core/res/res/layout/slice_secondary_text.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> + +<!-- 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. +--> +<!-- LinearLayout --> +<TextView + xmlns:android="http://schemas.android.com/apk/res/android" + android:textAppearance="?android:attr/textAppearanceSmall" + android:textColor="?android:attr/textColorSecondary" + android:gravity="center" + android:layout_height="wrap_content" + android:padding="4dp" + android:layout_width="match_parent" /> diff --git a/core/res/res/layout/slice_small_template.xml b/core/res/res/layout/slice_small_template.xml new file mode 100644 index 000000000000..cced42b48eef --- /dev/null +++ b/core/res/res/layout/slice_small_template.xml @@ -0,0 +1,67 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="?android:attr/listPreferredItemHeightSmall" + android:gravity="center_vertical" + android:paddingStart="?android:attr/listPreferredItemPaddingStart" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" + android:background="?android:attr/activatedBackgroundIndicator" + android:clipToPadding="false"> + + <LinearLayout + android:id="@android:id/icon_frame" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="-4dp" + android:gravity="start|center_vertical" + android:orientation="horizontal" + android:paddingEnd="12dp" + android:paddingTop="4dp" + android:paddingBottom="4dp"/> + + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_weight="1" + android:gravity="center_vertical" + android:orientation="vertical"> + + <TextView android:id="@android:id/title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:maxLines="2" + android:textAppearance="?android:attr/textAppearanceListItem" /> + + <TextView android:id="@android:id/summary" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignStart="@android:id/title" + android:textAppearance="?android:attr/textAppearanceListItemSecondary" + android:textColor="?android:attr/textColorSecondary" + android:maxLines="10" /> + + </LinearLayout> + + <LinearLayout android:id="@android:id/widget_frame" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:gravity="end|center_vertical" + android:orientation="horizontal" /> + +</LinearLayout> diff --git a/core/res/res/layout/slice_title.xml b/core/res/res/layout/slice_title.xml new file mode 100644 index 000000000000..455d59f1bfd5 --- /dev/null +++ b/core/res/res/layout/slice_title.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> + +<!-- 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. +--> + +<!-- LinearLayout --> +<TextView + xmlns:android="http://schemas.android.com/apk/res/android" + android:textAppearance="?android:attr/textAppearanceMedium" + android:textColor="?android:attr/textColorPrimary" + android:gravity="center" + android:layout_height="wrap_content" + android:padding="4dp" + android:layout_width="match_parent" /> diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml index b3d00535ecf8..14069e779939 100644 --- a/core/res/res/values/dimens.xml +++ b/core/res/res/values/dimens.xml @@ -617,4 +617,10 @@ --> <dimen name="autofill_save_icon_max_size">300dp</dimen> + <!-- Size of a slice shortcut view --> + <dimen name="slice_shortcut_size">56dp</dimen> + <!-- Size of action icons in a slice --> + <dimen name="slice_icon_size">24dp</dimen> + <!-- Standard padding used in a slice view --> + <dimen name="slice_padding">16dp</dimen> </resources> diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index 085f8dd6488d..5189e7fad0d5 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -4727,4 +4727,7 @@ <!-- Popup window default title to be read by a screen reader--> <string name="popup_window_default_title">Popup Window</string> + + <!-- Format string for indicating there is more content in a slice view --> + <string name="slice_more_content">+ <xliff:g id="number" example="5">%1$d</xliff:g></string> </resources> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index cc74f1799ceb..53d09d84b721 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -3100,4 +3100,19 @@ <java-symbol type="integer" name="config_stableDeviceDisplayWidth" /> <java-symbol type="integer" name="config_stableDeviceDisplayHeight" /> <java-symbol type="bool" name="config_display_no_service_when_sim_unready" /> + + <java-symbol type="layout" name="slice_grid" /> + <java-symbol type="layout" name="slice_message_local" /> + <java-symbol type="layout" name="slice_message" /> + <java-symbol type="layout" name="slice_title" /> + <java-symbol type="layout" name="slice_secondary_text" /> + <java-symbol type="layout" name="slice_remote_input" /> + <java-symbol type="layout" name="slice_small_template" /> + <java-symbol type="id" name="remote_input_progress" /> + <java-symbol type="id" name="remote_input_send" /> + <java-symbol type="id" name="remote_input" /> + <java-symbol type="dimen" name="slice_shortcut_size" /> + <java-symbol type="dimen" name="slice_icon_size" /> + <java-symbol type="dimen" name="slice_padding" /> + <java-symbol type="string" name="slice_more_content" /> </resources> |