1 /*
2  * Copyright 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.slice.widget;
18 
19 import static android.app.slice.Slice.HINT_NO_TINT;
20 import static android.app.slice.SliceItem.FORMAT_IMAGE;
21 import static android.app.slice.SliceItem.FORMAT_REMOTE_INPUT;
22 
23 import static androidx.slice.core.SliceHints.ICON_IMAGE;
24 
25 import android.app.PendingIntent.CanceledException;
26 import android.app.RemoteInput;
27 import android.app.slice.Slice;
28 import android.content.Context;
29 import android.content.res.ColorStateList;
30 import android.graphics.Color;
31 import android.os.Build;
32 import android.util.Log;
33 import android.util.TypedValue;
34 import android.view.View;
35 import android.view.ViewParent;
36 import android.widget.FrameLayout;
37 import android.widget.ImageView;
38 import android.widget.ImageView.ScaleType;
39 import android.widget.LinearLayout;
40 import android.widget.TextView;
41 
42 import androidx.annotation.NonNull;
43 import androidx.annotation.RequiresApi;
44 import androidx.annotation.RestrictTo;
45 import androidx.core.graphics.drawable.IconCompat;
46 import androidx.core.widget.ImageViewCompat;
47 import androidx.slice.SliceItem;
48 import androidx.slice.core.SliceActionImpl;
49 import androidx.slice.core.SliceQuery;
50 
51 import java.util.List;
52 
53 /**
54  * @hide
55  */
56 @RestrictTo(RestrictTo.Scope.LIBRARY)
57 public class ActionRow extends FrameLayout {
58 
59     private static final int MAX_ACTIONS = 5;
60     private static final String TAG = "ActionRow";
61 
62     private final int mSize;
63     private final int mIconPadding;
64     private final LinearLayout mActionsGroup;
65     private final boolean mFullActions;
66     private int mColor = Color.BLACK;
67 
ActionRow(Context context, boolean fullActions)68     public ActionRow(Context context, boolean fullActions) {
69         super(context);
70         mFullActions = fullActions;
71         mSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 48,
72                 context.getResources().getDisplayMetrics());
73         mIconPadding = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 12,
74                 context.getResources().getDisplayMetrics());
75         mActionsGroup = new LinearLayout(context);
76         mActionsGroup.setOrientation(LinearLayout.HORIZONTAL);
77         mActionsGroup.setLayoutParams(
78                 new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
79         addView(mActionsGroup);
80     }
81 
setColor(int color)82     private void setColor(int color) {
83         mColor = color;
84         for (int i = 0; i < mActionsGroup.getChildCount(); i++) {
85             View view = mActionsGroup.getChildAt(i);
86             int mode = (Integer) view.getTag();
87             boolean tint = mode == ICON_IMAGE;
88             if (tint) {
89                 ImageViewCompat.setImageTintList((ImageView) view, ColorStateList.valueOf(mColor));
90             }
91         }
92     }
93 
addAction(IconCompat icon, boolean allowTint)94     private ImageView addAction(IconCompat icon, boolean allowTint) {
95         ImageView imageView = new ImageView(getContext());
96         imageView.setPadding(mIconPadding, mIconPadding, mIconPadding, mIconPadding);
97         imageView.setScaleType(ScaleType.FIT_CENTER);
98         imageView.setImageDrawable(icon.loadDrawable(getContext()));
99         if (allowTint) {
100             ImageViewCompat.setImageTintList(imageView, ColorStateList.valueOf(mColor));
101         }
102         imageView.setBackground(SliceViewUtil.getDrawable(getContext(),
103                 android.R.attr.selectableItemBackground));
104         imageView.setTag(allowTint);
105         addAction(imageView);
106         return imageView;
107     }
108 
109     /**
110      * Set the actions and color for this action row.
111      */
setActions(@onNull List<SliceItem> actions, int color)112     public void setActions(@NonNull List<SliceItem> actions, int color) {
113         removeAllViews();
114         mActionsGroup.removeAllViews();
115         addView(mActionsGroup);
116         if (color != -1) {
117             setColor(color);
118         }
119         for (final SliceItem action : actions) {
120             if (mActionsGroup.getChildCount() >= MAX_ACTIONS) {
121                 return;
122             }
123             final SliceItem input = SliceQuery.find(action, FORMAT_REMOTE_INPUT);
124             final SliceItem image = SliceQuery.find(action, FORMAT_IMAGE);
125             if (input != null && image != null) {
126                 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
127                     handleSetRemoteInputActions(input, image, action);
128                 } else {
129                     Log.w(TAG, "Received RemoteInput on API <20 " + input);
130                 }
131             } else if (action.hasHint(Slice.HINT_SHORTCUT)) {
132                 final SliceActionImpl ac = new SliceActionImpl(action);
133                 IconCompat iconItem = ac.getIcon();
134                 if (iconItem != null && ac.getActionItem() != null) {
135                     boolean tint = ac.getImageMode() == ICON_IMAGE;
136                     addAction(iconItem, tint).setOnClickListener(
137                             new OnClickListener() {
138                                 @Override
139                                 public void onClick(View v) {
140                                     try {
141                                         // TODO - should log events here
142                                         ac.getActionItem().fireAction(null, null);
143                                     } catch (CanceledException e) {
144                                         e.printStackTrace();
145                                     }
146                                 }
147                             });
148                 }
149             }
150         }
151         setVisibility(getChildCount() != 0 ? View.VISIBLE : View.GONE);
152     }
153 
addAction(View child)154     private void addAction(View child) {
155         mActionsGroup.addView(child, new LinearLayout.LayoutParams(mSize, mSize, 1));
156     }
157 
158     @RequiresApi(21)
handleSetRemoteInputActions(final SliceItem input, SliceItem image, final SliceItem action)159     private void handleSetRemoteInputActions(final SliceItem input, SliceItem image,
160             final SliceItem action) {
161         if (input.getRemoteInput().getAllowFreeFormInput()) {
162             boolean tint = !image.hasHint(HINT_NO_TINT);
163             addAction(image.getIcon(), tint).setOnClickListener(
164                     new OnClickListener() {
165                         @Override
166                         public void onClick(View v) {
167                             handleRemoteInputClick(v, action,
168                                     input.getRemoteInput());
169                         }
170                     });
171             createRemoteInputView(mColor, getContext());
172         }
173     }
174 
175     @RequiresApi(21)
createRemoteInputView(int color, Context context)176     private void createRemoteInputView(int color, Context context) {
177         View riv = RemoteInputView.inflate(context, this);
178         riv.setVisibility(View.INVISIBLE);
179         addView(riv, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
180         riv.setBackgroundColor(color);
181     }
182 
183     @RequiresApi(21)
handleRemoteInputClick(View view, SliceItem action, RemoteInput input)184     private boolean handleRemoteInputClick(View view, SliceItem action,
185             RemoteInput input) {
186         if (input == null) {
187             return false;
188         }
189 
190         ViewParent p = view.getParent().getParent();
191         RemoteInputView riv = null;
192         while (p != null) {
193             if (p instanceof View) {
194                 View pv = (View) p;
195                 riv = findRemoteInputView(pv);
196                 if (riv != null) {
197                     break;
198                 }
199             }
200             p = p.getParent();
201         }
202         if (riv == null) {
203             return false;
204         }
205 
206         int width = view.getWidth();
207         if (view instanceof TextView) {
208             // Center the reveal on the text which might be off-center from the TextView
209             TextView tv = (TextView) view;
210             if (tv.getLayout() != null) {
211                 int innerWidth = (int) tv.getLayout().getLineWidth(0);
212                 innerWidth += tv.getCompoundPaddingLeft() + tv.getCompoundPaddingRight();
213                 width = Math.min(width, innerWidth);
214             }
215         }
216         int cx = view.getLeft() + width / 2;
217         int cy = view.getTop() + view.getHeight() / 2;
218         int w = riv.getWidth();
219         int h = riv.getHeight();
220         int r = Math.max(
221                 Math.max(cx + cy, cx + (h - cy)),
222                 Math.max((w - cx) + cy, (w - cx) + (h - cy)));
223 
224         riv.setRevealParameters(cx, cy, r);
225         riv.setAction(action);
226         riv.setRemoteInput(new RemoteInput[] {
227                 input
228         }, input);
229         riv.focusAnimated();
230         return true;
231     }
232 
233     @RequiresApi(21)
findRemoteInputView(View v)234     private RemoteInputView findRemoteInputView(View v) {
235         if (v == null) {
236             return null;
237         }
238         return (RemoteInputView) v.findViewWithTag(RemoteInputView.VIEW_TAG);
239     }
240 }
241