blob: fa1b64ceed97c899ab14bd86486b0e1e19be037e [file] [log] [blame]
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.app.slice.widget;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.StringDef;
import android.app.slice.Slice;
import android.app.slice.SliceItem;
import android.app.slice.SliceQuery;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.database.ContentObserver;
import android.graphics.drawable.ColorDrawable;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import com.android.internal.R;
import com.android.internal.util.Preconditions;
import java.util.List;
/**
* A view for displaying a {@link Slice} which is a piece of app content and actions. SliceView is
* able to present slice content in a templated format outside of the associated app. The way this
* content is displayed depends on the structure of the slice, the hints associated with the
* content, and the mode that SliceView is configured for. The modes that SliceView supports are:
* <ul>
* <li><b>Shortcut</b>: A shortcut is presented as an icon and a text label representing the main
* content or action associated with the slice.</li>
* <li><b>Small</b>: The small format has a restricted height and can present a single
* {@link SliceItem} or a limited collection of items.</li>
* <li><b>Large</b>: The large format displays multiple small templates in a list, if scrolling is
* not enabled (see {@link #setScrollable(boolean)}) the view will show as many items as it can
* comfortably fit.</li>
* </ul>
* <p>
* When constructing a slice, the contents of it can be annotated with hints, these provide the OS
* with some information on how the content should be displayed. For example, text annotated with
* {@link Slice#HINT_TITLE} would be placed in the title position of a template. A slice annotated
* with {@link Slice#HINT_LIST} would present the child items of that slice in a list.
* <p>
* SliceView can be provided a slice via a uri {@link #setSlice(Uri)} in which case a content
* observer will be set for that uri and the view will update if there are any changes to the slice.
* To use this the app must have a special permission to bind to the slice (see
* {@link android.Manifest.permission#BIND_SLICE}).
* <p>
* Example usage:
*
* <pre class="prettyprint">
* SliceView v = new SliceView(getContext());
* v.setMode(desiredMode);
* v.setSlice(sliceUri);
* </pre>
*/
public class SliceView extends ViewGroup {
private static final String TAG = "SliceView";
/**
* @hide
*/
public abstract static class SliceModeView extends FrameLayout {
public SliceModeView(Context context) {
super(context);
}
/**
* @return the mode of the slice being presented.
*/
public abstract String getMode();
/**
* @param slice the slice to show in this view.
*/
public abstract void setSlice(Slice slice);
}
/**
* @hide
*/
@StringDef({
MODE_SMALL, MODE_LARGE, MODE_SHORTCUT
})
public @interface SliceMode {}
/**
* Mode indicating this slice should be presented in small template format.
*/
public static final String MODE_SMALL = "SLICE_SMALL";
/**
* Mode indicating this slice should be presented in large template format.
*/
public static final String MODE_LARGE = "SLICE_LARGE";
/**
* Mode indicating this slice should be presented as an icon. A shortcut requires an intent,
* icon, and label. This can be indicated by using {@link Slice#HINT_TITLE} on an action in a
* slice.
*/
public static final String MODE_SHORTCUT = "SLICE_ICON";
/**
* Will select the type of slice binding based on size of the View. TODO: Put in some info about
* that selection.
*/
private static final String MODE_AUTO = "auto";
private String mMode = MODE_AUTO;
private SliceModeView mCurrentView;
private final ActionRow mActions;
private Slice mCurrentSlice;
private boolean mShowActions = true;
private boolean mIsScrollable;
private SliceObserver mObserver;
private final int mShortcutSize;
public SliceView(Context context) {
this(context, null);
}
public SliceView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public SliceView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public SliceView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
mObserver = new SliceObserver(new Handler(Looper.getMainLooper()));
mActions = new ActionRow(mContext, true);
mActions.setBackground(new ColorDrawable(0xffeeeeee));
mCurrentView = new LargeTemplateView(mContext);
addView(mCurrentView, getChildLp(mCurrentView));
addView(mActions, getChildLp(mActions));
mShortcutSize = getContext().getResources()
.getDimensionPixelSize(R.dimen.slice_shortcut_size);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
measureChildren(widthMeasureSpec, heightMeasureSpec);
int actionHeight = mActions.getVisibility() != View.GONE
? mActions.getMeasuredHeight()
: 0;
int newHeightSpec = MeasureSpec.makeMeasureSpec(
mCurrentView.getMeasuredHeight() + actionHeight, MeasureSpec.EXACTLY);
int width = MeasureSpec.getSize(widthMeasureSpec);
setMeasuredDimension(width, newHeightSpec);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
mCurrentView.layout(l, t, l + mCurrentView.getMeasuredWidth(),
t + mCurrentView.getMeasuredHeight());
if (mActions.getVisibility() != View.GONE) {
mActions.layout(l, mCurrentView.getMeasuredHeight(), l + mActions.getMeasuredWidth(),
mCurrentView.getMeasuredHeight() + mActions.getMeasuredHeight());
}
}
/**
* Populates this view with the {@link Slice} associated with the provided {@link Intent}. To
* use this method your app must have the permission
* {@link android.Manifest.permission#BIND_SLICE}).
* <p>
* Setting a slice differs from {@link #showSlice(Slice)} because it will ensure the view is
* updated with the slice identified by the provided intent changes. The lifecycle of this
* observer is handled by SliceView in {@link #onAttachedToWindow()} and
* {@link #onDetachedFromWindow()}. To unregister this observer outside of that you can call
* {@link #clearSlice}.
*
* @return true if a slice was found for the provided intent.
* @hide
*/
public boolean setSlice(@Nullable Intent intent) {
Slice s = Slice.bindSlice(mContext, intent);
if (s != null) {
return setSlice(s.getUri());
}
return s != null;
}
/**
* Populates this view with the {@link Slice} associated with the provided {@link Uri}. To use
* this method your app must have the permission
* {@link android.Manifest.permission#BIND_SLICE}).
* <p>
* Setting a slice differs from {@link #showSlice(Slice)} because it will ensure the view is
* updated when the slice identified by the provided URI changes. The lifecycle of this observer
* is handled by SliceView in {@link #onAttachedToWindow()} and {@link #onDetachedFromWindow()}.
* To unregister this observer outside of that you can call {@link #clearSlice}.
*
* @return true if a slice was found for the provided uri.
*/
public boolean setSlice(@NonNull Uri sliceUri) {
Preconditions.checkNotNull(sliceUri,
"Uri cannot be null, to remove the slice use clearSlice()");
if (sliceUri == null) {
clearSlice();
return false;
}
validate(sliceUri);
Slice s = Slice.bindSlice(mContext.getContentResolver(), sliceUri);
if (s != null) {
if (mObserver != null) {
getContext().getContentResolver().unregisterContentObserver(mObserver);
}
mObserver = new SliceObserver(new Handler(Looper.getMainLooper()));
if (isAttachedToWindow()) {
registerSlice(sliceUri);
}
mCurrentSlice = s;
reinflate();
}
return s != null;
}
/**
* Populates this view to the provided {@link Slice}.
* <p>
* This does not register a content observer on the URI that the slice is backed by so it will
* not update if the content changes. To have the view update when the content changes use
* {@link #setSlice(Uri)} instead. Unlike {@link #setSlice(Uri)}, this method does not require
* any special permissions.
*/
public void showSlice(@NonNull Slice slice) {
Preconditions.checkNotNull(slice,
"Slice cannot be null, to remove the slice use clearSlice()");
clearSlice();
mCurrentSlice = slice;
reinflate();
}
/**
* Unregisters the change observer that is set when using {@link #setSlice}. Normally this is
* done automatically during {@link #onDetachedFromWindow()}.
* <p>
* It is safe to call this method multiple times.
*/
public void clearSlice() {
mCurrentSlice = null;
if (mObserver != null) {
getContext().getContentResolver().unregisterContentObserver(mObserver);
mObserver = null;
}
}
/**
* Set the mode this view should present in.
*/
public void setMode(@SliceMode String mode) {
setMode(mode, false /* animate */);
}
/**
* Set whether this view should allow scrollable content when presenting in {@link #MODE_LARGE}.
*/
public void setScrollable(boolean isScrollable) {
mIsScrollable = isScrollable;
reinflate();
}
/**
* @hide
*/
public void setMode(@SliceMode String mode, boolean animate) {
if (animate) {
Log.e(TAG, "Animation not supported yet");
}
mMode = mode;
reinflate();
}
/**
* @return the mode this view is presenting in.
*/
public @SliceMode String getMode() {
if (mMode.equals(MODE_AUTO)) {
return MODE_LARGE;
}
return mMode;
}
/**
* @hide
*
* Whether this view should show a row of actions with it.
*/
public void setShowActionRow(boolean show) {
mShowActions = show;
reinflate();
}
private SliceModeView createView(String mode) {
switch (mode) {
case MODE_SHORTCUT:
return new ShortcutView(getContext());
case MODE_SMALL:
return new SmallTemplateView(getContext());
}
return new LargeTemplateView(getContext());
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
registerSlice(mCurrentSlice != null ? mCurrentSlice.getUri() : null);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (mObserver != null) {
getContext().getContentResolver().unregisterContentObserver(mObserver);
mObserver = null;
}
}
private void registerSlice(Uri sliceUri) {
if (sliceUri == null || mObserver == null) {
return;
}
mContext.getContentResolver().registerContentObserver(sliceUri,
false /* notifyForDescendants */, mObserver);
}
private void reinflate() {
if (mCurrentSlice == null) {
return;
}
// TODO: Smarter mapping here from one state to the next.
SliceItem color = SliceQuery.find(mCurrentSlice, SliceItem.TYPE_COLOR);
List<SliceItem> items = mCurrentSlice.getItems();
SliceItem actionRow = SliceQuery.find(mCurrentSlice, SliceItem.TYPE_SLICE,
Slice.HINT_ACTIONS,
Slice.HINT_ALT);
String mode = getMode();
if (!mode.equals(mCurrentView.getMode())) {
removeAllViews();
mCurrentView = createView(mode);
addView(mCurrentView, getChildLp(mCurrentView));
addView(mActions, getChildLp(mActions));
}
if (mode.equals(MODE_LARGE)) {
((LargeTemplateView) mCurrentView).setScrollable(mIsScrollable);
}
if (items.size() > 1 || (items.size() != 0 && items.get(0) != actionRow)) {
mCurrentView.setVisibility(View.VISIBLE);
mCurrentView.setSlice(mCurrentSlice);
} else {
mCurrentView.setVisibility(View.GONE);
}
boolean showActions = mShowActions && actionRow != null
&& !mode.equals(MODE_SHORTCUT);
if (showActions) {
mActions.setActions(actionRow, color);
mActions.setVisibility(View.VISIBLE);
} else {
mActions.setVisibility(View.GONE);
}
}
private LayoutParams getChildLp(View child) {
if (child instanceof ShortcutView) {
return new LayoutParams(mShortcutSize, mShortcutSize);
} else {
return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
}
}
private static void validate(Uri sliceUri) {
if (!ContentResolver.SCHEME_CONTENT.equals(sliceUri.getScheme())) {
throw new RuntimeException("Invalid uri " + sliceUri);
}
if (sliceUri.getPathSegments().size() == 0) {
throw new RuntimeException("Invalid uri " + sliceUri);
}
}
private class SliceObserver extends ContentObserver {
SliceObserver(Handler handler) {
super(handler);
}
@Override
public void onChange(boolean selfChange) {
this.onChange(selfChange, null);
}
@Override
public void onChange(boolean selfChange, Uri uri) {
Slice s = Slice.bindSlice(mContext.getContentResolver(), uri);
mCurrentSlice = s;
reinflate();
}
}
}