1 /*
2  * Copyright (C) 2015 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 android.view;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.compat.annotation.UnsupportedAppUsage;
22 import android.content.Context;
23 import android.content.res.Resources;
24 import android.graphics.Canvas;
25 import android.graphics.Outline;
26 import android.graphics.Rect;
27 import android.graphics.drawable.Drawable;
28 import android.os.Build;
29 import android.util.AttributeSet;
30 import android.widget.RelativeLayout;
31 import android.widget.RemoteViews;
32 import android.widget.TextView;
33 
34 import com.android.internal.R;
35 import com.android.internal.widget.CachingIconView;
36 import com.android.internal.widget.NotificationExpandButton;
37 
38 import java.util.ArrayList;
39 
40 /**
41  * A header of a notification view
42  *
43  * @hide
44  */
45 @RemoteViews.RemoteView
46 public class NotificationHeaderView extends RelativeLayout {
47     private final int mTouchableHeight;
48     private OnClickListener mExpandClickListener;
49     private HeaderTouchListener mTouchListener = new HeaderTouchListener();
50     private NotificationTopLineView mTopLineView;
51     private NotificationExpandButton mExpandButton;
52     private View mAltExpandTarget;
53     private CachingIconView mIcon;
54     private Drawable mBackground;
55     private boolean mEntireHeaderClickable;
56     private boolean mExpandOnlyOnButton;
57     private boolean mAcceptAllTouches;
58 
59     ViewOutlineProvider mProvider = new ViewOutlineProvider() {
60         @Override
61         public void getOutline(View view, Outline outline) {
62             if (mBackground != null) {
63                 outline.setRect(0, 0, getWidth(), getHeight());
64                 outline.setAlpha(1f);
65             }
66         }
67     };
68 
NotificationHeaderView(Context context)69     public NotificationHeaderView(Context context) {
70         this(context, null);
71     }
72 
73     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
NotificationHeaderView(Context context, @Nullable AttributeSet attrs)74     public NotificationHeaderView(Context context, @Nullable AttributeSet attrs) {
75         this(context, attrs, 0);
76     }
77 
NotificationHeaderView(Context context, @Nullable AttributeSet attrs, int defStyleAttr)78     public NotificationHeaderView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
79         this(context, attrs, defStyleAttr, 0);
80     }
81 
NotificationHeaderView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)82     public NotificationHeaderView(Context context, AttributeSet attrs, int defStyleAttr,
83             int defStyleRes) {
84         super(context, attrs, defStyleAttr, defStyleRes);
85         Resources res = getResources();
86         mTouchableHeight = res.getDimensionPixelSize(R.dimen.notification_header_touchable_height);
87         mEntireHeaderClickable = res.getBoolean(R.bool.config_notificationHeaderClickableForExpand);
88     }
89 
90     @Override
onFinishInflate()91     protected void onFinishInflate() {
92         super.onFinishInflate();
93         mIcon = findViewById(R.id.icon);
94         mTopLineView = findViewById(R.id.notification_top_line);
95         mExpandButton = findViewById(R.id.expand_button);
96         mAltExpandTarget = findViewById(R.id.alternate_expand_target);
97         setClipToPadding(false);
98     }
99 
100     /**
101      * Set a {@link Drawable} to be displayed as a background on the header.
102      */
setHeaderBackgroundDrawable(Drawable drawable)103     public void setHeaderBackgroundDrawable(Drawable drawable) {
104         if (drawable != null) {
105             setWillNotDraw(false);
106             mBackground = drawable;
107             mBackground.setCallback(this);
108             setOutlineProvider(mProvider);
109         } else {
110             setWillNotDraw(true);
111             mBackground = null;
112             setOutlineProvider(null);
113         }
114         invalidate();
115     }
116 
117     @Override
onDraw(Canvas canvas)118     protected void onDraw(Canvas canvas) {
119         if (mBackground != null) {
120             mBackground.setBounds(0, 0, getWidth(), getHeight());
121             mBackground.draw(canvas);
122         }
123     }
124 
125     @Override
verifyDrawable(@onNull Drawable who)126     protected boolean verifyDrawable(@NonNull Drawable who) {
127         return super.verifyDrawable(who) || who == mBackground;
128     }
129 
130     @Override
drawableStateChanged()131     protected void drawableStateChanged() {
132         if (mBackground != null && mBackground.isStateful()) {
133             mBackground.setState(getDrawableState());
134         }
135     }
136 
updateTouchListener()137     private void updateTouchListener() {
138         if (mExpandClickListener == null) {
139             setOnTouchListener(null);
140             return;
141         }
142         setOnTouchListener(mTouchListener);
143         mTouchListener.bindTouchRects();
144     }
145 
146     @Override
setOnClickListener(@ullable OnClickListener l)147     public void setOnClickListener(@Nullable OnClickListener l) {
148         mExpandClickListener = l;
149         mExpandButton.setOnClickListener(mExpandClickListener);
150         mAltExpandTarget.setOnClickListener(mExpandClickListener);
151         updateTouchListener();
152     }
153 
154     /**
155      * Sets the extra margin at the end of the top line of left-aligned text + icons.
156      * This value will have the margin required to accommodate the expand button added to it.
157      *
158      * @param extraMarginEnd extra margin in px
159      */
setTopLineExtraMarginEnd(int extraMarginEnd)160     public void setTopLineExtraMarginEnd(int extraMarginEnd) {
161         mTopLineView.setHeaderTextMarginEnd(extraMarginEnd);
162     }
163 
164     /**
165      * Sets the extra margin at the end of the top line of left-aligned text + icons.
166      * This value will have the margin required to accommodate the expand button added to it.
167      *
168      * @param extraMarginEndDp extra margin in dp
169      */
170     @RemotableViewMethod
setTopLineExtraMarginEndDp(float extraMarginEndDp)171     public void setTopLineExtraMarginEndDp(float extraMarginEndDp) {
172         setTopLineExtraMarginEnd(
173                 (int) (extraMarginEndDp * getResources().getDisplayMetrics().density));
174     }
175 
176     /**
177      * This is used to make the low-priority header show the bolded text of a title.
178      *
179      * @param styleTextAsTitle true if this header's text is to have the style of a title
180      */
181     @RemotableViewMethod
styleTextAsTitle(boolean styleTextAsTitle)182     public void styleTextAsTitle(boolean styleTextAsTitle) {
183         int styleResId = styleTextAsTitle
184                 ? R.style.TextAppearance_DeviceDefault_Notification_Title
185                 : R.style.TextAppearance_DeviceDefault_Notification_Info;
186         // Most of the time, we're showing text in the minimized state
187         View headerText = findViewById(R.id.header_text);
188         if (headerText instanceof TextView) {
189             ((TextView) headerText).setTextAppearance(styleResId);
190         }
191         // If there's no summary or text, we show the app name instead of nothing
192         View appNameText = findViewById(R.id.app_name_text);
193         if (appNameText instanceof TextView) {
194             ((TextView) appNameText).setTextAppearance(styleResId);
195         }
196     }
197 
198     /**
199      * Handles clicks on the header based on the region tapped.
200      */
201     public class HeaderTouchListener implements OnTouchListener {
202 
203         private final ArrayList<Rect> mTouchRects = new ArrayList<>();
204         private Rect mExpandButtonRect;
205         private Rect mAltExpandTargetRect;
206         private int mTouchSlop;
207         private boolean mTrackGesture;
208         private float mDownX;
209         private float mDownY;
210 
HeaderTouchListener()211         public HeaderTouchListener() {
212         }
213 
bindTouchRects()214         public void bindTouchRects() {
215             mTouchRects.clear();
216             addRectAroundView(mIcon);
217             mExpandButtonRect = addRectAroundView(mExpandButton);
218             mAltExpandTargetRect = addRectAroundView(mAltExpandTarget);
219             addWidthRect();
220             mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
221         }
222 
addWidthRect()223         private void addWidthRect() {
224             Rect r = new Rect();
225             r.top = 0;
226             r.bottom = mTouchableHeight;
227             r.left = 0;
228             r.right = getWidth();
229             mTouchRects.add(r);
230         }
231 
addRectAroundView(View view)232         private Rect addRectAroundView(View view) {
233             final Rect r = getRectAroundView(view);
234             mTouchRects.add(r);
235             return r;
236         }
237 
getRectAroundView(View view)238         private Rect getRectAroundView(View view) {
239             float size = 48 * getResources().getDisplayMetrics().density;
240             float width = Math.max(size, view.getWidth());
241             float height = Math.max(size, view.getHeight());
242             final Rect r = new Rect();
243             if (view.getVisibility() == GONE) {
244                 view = getFirstChildNotGone();
245                 r.left = (int) (view.getLeft() - width / 2.0f);
246             } else {
247                 r.left = (int) ((view.getLeft() + view.getRight()) / 2.0f - width / 2.0f);
248             }
249             r.top = (int) ((view.getTop() + view.getBottom()) / 2.0f - height / 2.0f);
250             r.bottom = (int) (r.top + height);
251             r.right = (int) (r.left + width);
252             return r;
253         }
254 
255         @Override
onTouch(View v, MotionEvent event)256         public boolean onTouch(View v, MotionEvent event) {
257             float x = event.getX();
258             float y = event.getY();
259             switch (event.getActionMasked() & MotionEvent.ACTION_MASK) {
260                 case MotionEvent.ACTION_DOWN:
261                     mTrackGesture = false;
262                     if (isInside(x, y)) {
263                         mDownX = x;
264                         mDownY = y;
265                         mTrackGesture = true;
266                         return true;
267                     }
268                     break;
269                 case MotionEvent.ACTION_MOVE:
270                     if (mTrackGesture) {
271                         if (Math.abs(mDownX - x) > mTouchSlop
272                                 || Math.abs(mDownY - y) > mTouchSlop) {
273                             mTrackGesture = false;
274                         }
275                     }
276                     break;
277                 case MotionEvent.ACTION_UP:
278                     if (mTrackGesture) {
279                         float topLineX = mTopLineView.getX();
280                         float topLineY = mTopLineView.getY();
281                         if (mTopLineView.onTouchUp(x - topLineX, y - topLineY,
282                                 mDownX - topLineX, mDownY - topLineY)) {
283                             break;
284                         }
285                         mExpandButton.performClick();
286                     }
287                     break;
288             }
289             return mTrackGesture;
290         }
291 
isInside(float x, float y)292         private boolean isInside(float x, float y) {
293             if (mAcceptAllTouches) {
294                 return true;
295             }
296             if (mExpandOnlyOnButton) {
297                 return mExpandButtonRect.contains((int) x, (int) y)
298                         || mAltExpandTargetRect.contains((int) x, (int) y);
299             }
300             for (int i = 0; i < mTouchRects.size(); i++) {
301                 Rect r = mTouchRects.get(i);
302                 if (r.contains((int) x, (int) y)) {
303                     return true;
304                 }
305             }
306             float topLineX = x - mTopLineView.getX();
307             float topLineY = y - mTopLineView.getY();
308             return mTopLineView.isInTouchRect(topLineX, topLineY);
309         }
310     }
311 
getFirstChildNotGone()312     private View getFirstChildNotGone() {
313         for (int i = 0; i < getChildCount(); i++) {
314             final View child = getChildAt(i);
315             if (child.getVisibility() != GONE) {
316                 return child;
317             }
318         }
319         return this;
320     }
321 
322     @Override
hasOverlappingRendering()323     public boolean hasOverlappingRendering() {
324         return false;
325     }
326 
isInTouchRect(float x, float y)327     public boolean isInTouchRect(float x, float y) {
328         if (mExpandClickListener == null) {
329             return false;
330         }
331         return mTouchListener.isInside(x, y);
332     }
333 
334     /**
335      * Sets whether or not all touches to this header view will register as a click. Note that
336      * if the config value for {@code config_notificationHeaderClickableForExpand} is {@code true},
337      * then calling this method with {@code false} will not override that configuration.
338      */
339     @RemotableViewMethod
setAcceptAllTouches(boolean acceptAllTouches)340     public void setAcceptAllTouches(boolean acceptAllTouches) {
341         mAcceptAllTouches = mEntireHeaderClickable || acceptAllTouches;
342     }
343 
344     /**
345      * Sets whether only the expand icon itself should serve as the expand target.
346      */
347     @RemotableViewMethod
setExpandOnlyOnButton(boolean expandOnlyOnButton)348     public void setExpandOnlyOnButton(boolean expandOnlyOnButton) {
349         mExpandOnlyOnButton = expandOnlyOnButton;
350     }
351 }
352