1 /*
2  * Copyright (C) 2022 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 com.android.systemui.clipboardoverlay;
18 
19 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
20 
21 import static com.android.systemui.Flags.screenshotShelfUi2;
22 
23 import android.animation.Animator;
24 import android.animation.AnimatorListenerAdapter;
25 import android.animation.AnimatorSet;
26 import android.animation.ObjectAnimator;
27 import android.animation.TimeInterpolator;
28 import android.animation.ValueAnimator;
29 import android.annotation.Nullable;
30 import android.app.PendingIntent;
31 import android.app.RemoteAction;
32 import android.content.Context;
33 import android.content.res.Resources;
34 import android.graphics.Bitmap;
35 import android.graphics.Insets;
36 import android.graphics.Paint;
37 import android.graphics.Rect;
38 import android.graphics.Region;
39 import android.graphics.drawable.Icon;
40 import android.util.AttributeSet;
41 import android.util.DisplayMetrics;
42 import android.util.Log;
43 import android.util.MathUtils;
44 import android.util.TypedValue;
45 import android.view.DisplayCutout;
46 import android.view.Gravity;
47 import android.view.LayoutInflater;
48 import android.view.View;
49 import android.view.WindowInsets;
50 import android.view.accessibility.AccessibilityManager;
51 import android.view.animation.LinearInterpolator;
52 import android.view.animation.PathInterpolator;
53 import android.widget.FrameLayout;
54 import android.widget.ImageView;
55 import android.widget.LinearLayout;
56 import android.widget.TextView;
57 
58 import androidx.core.view.ViewCompat;
59 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
60 
61 import com.android.systemui.res.R;
62 import com.android.systemui.screenshot.DraggableConstraintLayout;
63 import com.android.systemui.screenshot.FloatingWindowUtil;
64 import com.android.systemui.screenshot.OverlayActionChip;
65 import com.android.systemui.screenshot.ui.binder.ActionButtonViewBinder;
66 import com.android.systemui.screenshot.ui.viewmodel.ActionButtonAppearance;
67 import com.android.systemui.screenshot.ui.viewmodel.ActionButtonViewModel;
68 
69 import kotlin.Unit;
70 import kotlin.jvm.functions.Function0;
71 
72 import java.util.ArrayList;
73 
74 /**
75  * Handles the visual elements and animations for the clipboard overlay.
76  */
77 public class ClipboardOverlayView extends DraggableConstraintLayout {
78 
79     interface ClipboardOverlayCallbacks extends SwipeDismissCallbacks {
onDismissButtonTapped()80         void onDismissButtonTapped();
81 
onRemoteCopyButtonTapped()82         void onRemoteCopyButtonTapped();
83 
onShareButtonTapped()84         void onShareButtonTapped();
85 
onPreviewTapped()86         void onPreviewTapped();
87 
onMinimizedViewTapped()88         void onMinimizedViewTapped();
89     }
90 
91     private static final String TAG = "ClipboardView";
92 
93     private static final int SWIPE_PADDING_DP = 12; // extra padding around views to allow swipe
94     private static final int FONT_SEARCH_STEP_PX = 4;
95 
96     private final DisplayMetrics mDisplayMetrics;
97     private final AccessibilityManager mAccessibilityManager;
98     private final ArrayList<View> mActionChips = new ArrayList<>();
99 
100     private View mClipboardPreview;
101     private ImageView mImagePreview;
102     private TextView mTextPreview;
103     private TextView mHiddenPreview;
104     private LinearLayout mMinimizedPreview;
105     private View mPreviewBorder;
106     private View mShareChip;
107     private View mRemoteCopyChip;
108     private View mActionContainerBackground;
109     private View mDismissButton;
110     private LinearLayout mActionContainer;
111     private ClipboardOverlayCallbacks mClipboardCallbacks;
112     private ActionButtonViewBinder mActionButtonViewBinder = new ActionButtonViewBinder();
113 
ClipboardOverlayView(Context context)114     public ClipboardOverlayView(Context context) {
115         this(context, null);
116     }
117 
ClipboardOverlayView(Context context, AttributeSet attrs)118     public ClipboardOverlayView(Context context, AttributeSet attrs) {
119         this(context, attrs, 0);
120     }
121 
ClipboardOverlayView(Context context, AttributeSet attrs, int defStyleAttr)122     public ClipboardOverlayView(Context context, AttributeSet attrs, int defStyleAttr) {
123         super(context, attrs, defStyleAttr);
124         mDisplayMetrics = new DisplayMetrics();
125         mContext.getDisplay().getRealMetrics(mDisplayMetrics);
126         mAccessibilityManager = AccessibilityManager.getInstance(mContext);
127     }
128 
129     @Override
onFinishInflate()130     protected void onFinishInflate() {
131         mActionContainerBackground = requireViewById(R.id.actions_container_background);
132         mActionContainer = requireViewById(R.id.actions);
133         mClipboardPreview = requireViewById(R.id.clipboard_preview);
134         mPreviewBorder = requireViewById(R.id.preview_border);
135         mImagePreview = requireViewById(R.id.image_preview);
136         mTextPreview = requireViewById(R.id.text_preview);
137         mHiddenPreview = requireViewById(R.id.hidden_preview);
138         mMinimizedPreview = requireViewById(R.id.minimized_preview);
139         mShareChip = requireViewById(R.id.share_chip);
140         mRemoteCopyChip = requireViewById(R.id.remote_copy_chip);
141         mDismissButton = requireViewById(R.id.dismiss_button);
142 
143         bindDefaultActionChips();
144 
145         mTextPreview.getViewTreeObserver().addOnPreDrawListener(() -> {
146             int availableHeight = mTextPreview.getHeight()
147                     - (mTextPreview.getPaddingTop() + mTextPreview.getPaddingBottom());
148             mTextPreview.setMaxLines(Math.max(availableHeight / mTextPreview.getLineHeight(), 1));
149             return true;
150         });
151         super.onFinishInflate();
152     }
153 
bindDefaultActionChips()154     private void bindDefaultActionChips() {
155         if (screenshotShelfUi2()) {
156             mActionButtonViewBinder.bind(mRemoteCopyChip,
157                     ActionButtonViewModel.Companion.withNextId(
158                             new ActionButtonAppearance(
159                                     Icon.createWithResource(mContext,
160                                             R.drawable.ic_baseline_devices_24).loadDrawable(
161                                             mContext),
162                                     null,
163                                     mContext.getString(R.string.clipboard_send_nearby_description),
164                                     true),
165                             new Function0<>() {
166                                 @Override
167                                 public Unit invoke() {
168                                     if (mClipboardCallbacks != null) {
169                                         mClipboardCallbacks.onRemoteCopyButtonTapped();
170                                     }
171                                     return null;
172                                 }
173                             }));
174             mActionButtonViewBinder.bind(mShareChip,
175                     ActionButtonViewModel.Companion.withNextId(
176                             new ActionButtonAppearance(
177                                     Icon.createWithResource(mContext,
178                                             R.drawable.ic_screenshot_share).loadDrawable(mContext),
179                                     null,
180                                     mContext.getString(com.android.internal.R.string.share),
181                                     true),
182                             new Function0<>() {
183                                 @Override
184                                 public Unit invoke() {
185                                     if (mClipboardCallbacks != null) {
186                                         mClipboardCallbacks.onShareButtonTapped();
187                                     }
188                                     return null;
189                                 }
190                             }));
191         } else {
192             mShareChip.setAlpha(1);
193             mRemoteCopyChip.setAlpha(1);
194 
195             ((ImageView) mRemoteCopyChip.findViewById(R.id.overlay_action_chip_icon)).setImageIcon(
196                     Icon.createWithResource(mContext, R.drawable.ic_baseline_devices_24));
197             ((ImageView) mShareChip.findViewById(R.id.overlay_action_chip_icon)).setImageIcon(
198                     Icon.createWithResource(mContext, R.drawable.ic_screenshot_share));
199 
200             mShareChip.setContentDescription(
201                     mContext.getString(com.android.internal.R.string.share));
202             mRemoteCopyChip.setContentDescription(
203                     mContext.getString(R.string.clipboard_send_nearby_description));
204         }
205     }
206 
207     @Override
setCallbacks(SwipeDismissCallbacks callbacks)208     public void setCallbacks(SwipeDismissCallbacks callbacks) {
209         super.setCallbacks(callbacks);
210         ClipboardOverlayCallbacks clipboardCallbacks = (ClipboardOverlayCallbacks) callbacks;
211         if (!screenshotShelfUi2()) {
212             mShareChip.setOnClickListener(v -> clipboardCallbacks.onShareButtonTapped());
213             mRemoteCopyChip.setOnClickListener(v -> clipboardCallbacks.onRemoteCopyButtonTapped());
214         }
215         mDismissButton.setOnClickListener(v -> clipboardCallbacks.onDismissButtonTapped());
216         mClipboardPreview.setOnClickListener(v -> clipboardCallbacks.onPreviewTapped());
217         mMinimizedPreview.setOnClickListener(v -> clipboardCallbacks.onMinimizedViewTapped());
218         mClipboardCallbacks = clipboardCallbacks;
219     }
220 
setEditAccessibilityAction(boolean editable)221     void setEditAccessibilityAction(boolean editable) {
222         if (editable) {
223             ViewCompat.replaceAccessibilityAction(mClipboardPreview,
224                     AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK,
225                     mContext.getString(R.string.clipboard_edit), null);
226         } else {
227             ViewCompat.replaceAccessibilityAction(mClipboardPreview,
228                     AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK,
229                     null, null);
230         }
231     }
232 
setMinimized(boolean minimized)233     void setMinimized(boolean minimized) {
234         if (minimized) {
235             mMinimizedPreview.setVisibility(View.VISIBLE);
236             mClipboardPreview.setVisibility(View.GONE);
237             mPreviewBorder.setVisibility(View.GONE);
238             mActionContainer.setVisibility(View.GONE);
239             mActionContainerBackground.setVisibility(View.GONE);
240         } else {
241             mMinimizedPreview.setVisibility(View.GONE);
242             mClipboardPreview.setVisibility(View.VISIBLE);
243             mPreviewBorder.setVisibility(View.VISIBLE);
244             mActionContainer.setVisibility(View.VISIBLE);
245         }
246     }
247 
setInsets(WindowInsets insets, int orientation)248     void setInsets(WindowInsets insets, int orientation) {
249         FrameLayout.LayoutParams p = (FrameLayout.LayoutParams) getLayoutParams();
250         if (p == null) {
251             return;
252         }
253         Rect margins = computeMargins(insets, orientation);
254 
255         p.setMargins(margins.left, margins.top, margins.right, margins.bottom);
256         setLayoutParams(p);
257         requestLayout();
258     }
259 
isInTouchRegion(int x, int y)260     boolean isInTouchRegion(int x, int y) {
261         Region touchRegion = new Region();
262         final Rect tmpRect = new Rect();
263 
264         mPreviewBorder.getBoundsOnScreen(tmpRect);
265         tmpRect.inset(
266                 (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP),
267                 (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP));
268         touchRegion.op(tmpRect, Region.Op.UNION);
269 
270         mActionContainerBackground.getBoundsOnScreen(tmpRect);
271         tmpRect.inset(
272                 (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP),
273                 (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP));
274         touchRegion.op(tmpRect, Region.Op.UNION);
275 
276         mMinimizedPreview.getBoundsOnScreen(tmpRect);
277         tmpRect.inset(
278                 (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP),
279                 (int) FloatingWindowUtil.dpToPx(mDisplayMetrics, -SWIPE_PADDING_DP));
280         touchRegion.op(tmpRect, Region.Op.UNION);
281 
282         mDismissButton.getBoundsOnScreen(tmpRect);
283         touchRegion.op(tmpRect, Region.Op.UNION);
284 
285         return touchRegion.contains(x, y);
286     }
287 
setRemoteCopyVisibility(boolean visible)288     void setRemoteCopyVisibility(boolean visible) {
289         if (visible) {
290             mRemoteCopyChip.setVisibility(View.VISIBLE);
291             mActionContainerBackground.setVisibility(View.VISIBLE);
292         } else {
293             mRemoteCopyChip.setVisibility(View.GONE);
294         }
295     }
296 
showDefaultTextPreview()297     void showDefaultTextPreview() {
298         String copied = mContext.getString(R.string.clipboard_overlay_text_copied);
299         showTextPreview(copied, false);
300     }
301 
showTextPreview(CharSequence text, boolean hidden)302     void showTextPreview(CharSequence text, boolean hidden) {
303         TextView textView = hidden ? mHiddenPreview : mTextPreview;
304         showSinglePreview(textView);
305         textView.setText(text.subSequence(0, Math.min(500, text.length())));
306         updateTextSize(text, textView);
307         textView.addOnLayoutChangeListener(
308                 (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
309                     if (right - left != oldRight - oldLeft) {
310                         updateTextSize(text, textView);
311                     }
312                 });
313     }
314 
getPreview()315     View getPreview() {
316         return mClipboardPreview;
317     }
318 
showImagePreview(@ullable Bitmap thumbnail)319     void showImagePreview(@Nullable Bitmap thumbnail) {
320         if (thumbnail == null) {
321             mHiddenPreview.setText(mContext.getString(R.string.clipboard_text_hidden));
322             showSinglePreview(mHiddenPreview);
323         } else {
324             mImagePreview.setImageBitmap(thumbnail);
325             showSinglePreview(mImagePreview);
326         }
327     }
328 
showShareChip()329     void showShareChip() {
330         mShareChip.setVisibility(View.VISIBLE);
331         mActionContainerBackground.setVisibility(View.VISIBLE);
332     }
333 
reset()334     void reset() {
335         setTranslationX(0);
336         setAlpha(0);
337         mActionContainerBackground.setVisibility(View.GONE);
338         mDismissButton.setVisibility(View.GONE);
339         mShareChip.setVisibility(View.GONE);
340         mRemoteCopyChip.setVisibility(View.GONE);
341         setEditAccessibilityAction(false);
342         resetActionChips();
343     }
344 
resetActionChips()345     void resetActionChips() {
346         for (View chip : mActionChips) {
347             mActionContainer.removeView(chip);
348         }
349         mActionChips.clear();
350     }
351 
getMinimizedFadeoutAnimation()352     Animator getMinimizedFadeoutAnimation() {
353         ObjectAnimator anim = ObjectAnimator.ofFloat(mMinimizedPreview, "alpha", 1, 0);
354         anim.setDuration(66);
355         anim.addListener(new AnimatorListenerAdapter() {
356             @Override
357             public void onAnimationEnd(Animator animation) {
358                 super.onAnimationEnd(animation);
359                 mMinimizedPreview.setVisibility(View.GONE);
360                 mMinimizedPreview.setAlpha(1);
361             }
362         });
363         return anim;
364     }
365 
getEnterAnimation()366     Animator getEnterAnimation() {
367         if (mAccessibilityManager.isEnabled()) {
368             mDismissButton.setVisibility(View.VISIBLE);
369         }
370         TimeInterpolator linearInterpolator = new LinearInterpolator();
371         TimeInterpolator scaleInterpolator = new PathInterpolator(0, 0, 0, 1f);
372         AnimatorSet enterAnim = new AnimatorSet();
373 
374         ValueAnimator rootAnim = ValueAnimator.ofFloat(0, 1);
375         rootAnim.setInterpolator(linearInterpolator);
376         rootAnim.setDuration(66);
377         rootAnim.addUpdateListener(animation -> {
378             setAlpha(animation.getAnimatedFraction());
379         });
380 
381         ValueAnimator scaleAnim = ValueAnimator.ofFloat(0, 1);
382         scaleAnim.setInterpolator(scaleInterpolator);
383         scaleAnim.setDuration(333);
384         scaleAnim.addUpdateListener(animation -> {
385             float previewScale = MathUtils.lerp(.9f, 1f, animation.getAnimatedFraction());
386             mMinimizedPreview.setScaleX(previewScale);
387             mMinimizedPreview.setScaleY(previewScale);
388             mClipboardPreview.setScaleX(previewScale);
389             mClipboardPreview.setScaleY(previewScale);
390             mPreviewBorder.setScaleX(previewScale);
391             mPreviewBorder.setScaleY(previewScale);
392 
393             float pivotX = mClipboardPreview.getWidth() / 2f + mClipboardPreview.getX();
394             mActionContainerBackground.setPivotX(pivotX - mActionContainerBackground.getX());
395             mActionContainer.setPivotX(pivotX - ((View) mActionContainer.getParent()).getX());
396             float actionsScaleX = MathUtils.lerp(.7f, 1f, animation.getAnimatedFraction());
397             float actionsScaleY = MathUtils.lerp(.9f, 1f, animation.getAnimatedFraction());
398             mActionContainer.setScaleX(actionsScaleX);
399             mActionContainer.setScaleY(actionsScaleY);
400             mActionContainerBackground.setScaleX(actionsScaleX);
401             mActionContainerBackground.setScaleY(actionsScaleY);
402         });
403 
404         ValueAnimator alphaAnim = ValueAnimator.ofFloat(0, 1);
405         alphaAnim.setInterpolator(linearInterpolator);
406         alphaAnim.setDuration(283);
407         alphaAnim.addUpdateListener(animation -> {
408             float alpha = animation.getAnimatedFraction();
409             mMinimizedPreview.setAlpha(alpha);
410             mClipboardPreview.setAlpha(alpha);
411             mPreviewBorder.setAlpha(alpha);
412             mDismissButton.setAlpha(alpha);
413             mActionContainer.setAlpha(alpha);
414         });
415 
416         mMinimizedPreview.setAlpha(0);
417         mActionContainer.setAlpha(0);
418         mPreviewBorder.setAlpha(0);
419         mClipboardPreview.setAlpha(0);
420         enterAnim.play(rootAnim).with(scaleAnim);
421         enterAnim.play(alphaAnim).after(50).after(rootAnim);
422 
423         enterAnim.addListener(new AnimatorListenerAdapter() {
424             @Override
425             public void onAnimationEnd(Animator animation) {
426                 super.onAnimationEnd(animation);
427                 setAlpha(1);
428             }
429         });
430         return enterAnim;
431     }
432 
getFadeOutAnimation()433     Animator getFadeOutAnimation() {
434         ValueAnimator alphaAnim = ValueAnimator.ofFloat(1, 0);
435         alphaAnim.addUpdateListener(animation -> {
436             float alpha = (float) animation.getAnimatedValue();
437             mActionContainer.setAlpha(alpha);
438             mActionContainerBackground.setAlpha(alpha);
439             mPreviewBorder.setAlpha(alpha);
440             mDismissButton.setAlpha(alpha);
441         });
442         alphaAnim.setDuration(300);
443         return alphaAnim;
444     }
445 
getExitAnimation()446     Animator getExitAnimation() {
447         TimeInterpolator linearInterpolator = new LinearInterpolator();
448         TimeInterpolator scaleInterpolator = new PathInterpolator(.3f, 0, 1f, 1f);
449         AnimatorSet exitAnim = new AnimatorSet();
450 
451         ValueAnimator rootAnim = ValueAnimator.ofFloat(0, 1);
452         rootAnim.setInterpolator(linearInterpolator);
453         rootAnim.setDuration(100);
454         rootAnim.addUpdateListener(anim -> setAlpha(1 - anim.getAnimatedFraction()));
455 
456         ValueAnimator scaleAnim = ValueAnimator.ofFloat(0, 1);
457         scaleAnim.setInterpolator(scaleInterpolator);
458         scaleAnim.setDuration(250);
459         scaleAnim.addUpdateListener(animation -> {
460             float previewScale = MathUtils.lerp(1f, .9f, animation.getAnimatedFraction());
461             mMinimizedPreview.setScaleX(previewScale);
462             mMinimizedPreview.setScaleY(previewScale);
463             mClipboardPreview.setScaleX(previewScale);
464             mClipboardPreview.setScaleY(previewScale);
465             mPreviewBorder.setScaleX(previewScale);
466             mPreviewBorder.setScaleY(previewScale);
467 
468             float pivotX = mClipboardPreview.getWidth() / 2f + mClipboardPreview.getX();
469             mActionContainerBackground.setPivotX(pivotX - mActionContainerBackground.getX());
470             mActionContainer.setPivotX(pivotX - ((View) mActionContainer.getParent()).getX());
471             float actionScaleX = MathUtils.lerp(1f, .8f, animation.getAnimatedFraction());
472             float actionScaleY = MathUtils.lerp(1f, .9f, animation.getAnimatedFraction());
473             mActionContainer.setScaleX(actionScaleX);
474             mActionContainer.setScaleY(actionScaleY);
475             mActionContainerBackground.setScaleX(actionScaleX);
476             mActionContainerBackground.setScaleY(actionScaleY);
477         });
478 
479         ValueAnimator alphaAnim = ValueAnimator.ofFloat(0, 1);
480         alphaAnim.setInterpolator(linearInterpolator);
481         alphaAnim.setDuration(166);
482         alphaAnim.addUpdateListener(animation -> {
483             float alpha = 1 - animation.getAnimatedFraction();
484             mMinimizedPreview.setAlpha(alpha);
485             mClipboardPreview.setAlpha(alpha);
486             mPreviewBorder.setAlpha(alpha);
487             mDismissButton.setAlpha(alpha);
488             mActionContainer.setAlpha(alpha);
489         });
490 
491         exitAnim.play(alphaAnim).with(scaleAnim);
492         exitAnim.play(rootAnim).after(150).after(alphaAnim);
493         return exitAnim;
494     }
495 
setActionChip(RemoteAction action, Runnable onFinish)496     void setActionChip(RemoteAction action, Runnable onFinish) {
497         mActionContainerBackground.setVisibility(View.VISIBLE);
498         View chip;
499         if (screenshotShelfUi2()) {
500             chip = constructShelfActionChip(action, onFinish);
501         } else {
502             chip = constructActionChip(action, onFinish);
503         }
504         mActionContainer.addView(chip);
505         mActionChips.add(chip);
506     }
507 
showSinglePreview(View v)508     private void showSinglePreview(View v) {
509         mTextPreview.setVisibility(View.GONE);
510         mImagePreview.setVisibility(View.GONE);
511         mHiddenPreview.setVisibility(View.GONE);
512         mMinimizedPreview.setVisibility(View.GONE);
513         v.setVisibility(View.VISIBLE);
514     }
515 
constructShelfActionChip(RemoteAction action, Runnable onFinish)516     private View constructShelfActionChip(RemoteAction action, Runnable onFinish) {
517         View chip = LayoutInflater.from(mContext).inflate(
518                 R.layout.shelf_action_chip, mActionContainer, false);
519         mActionButtonViewBinder.bind(chip, ActionButtonViewModel.Companion.withNextId(
520                 new ActionButtonAppearance(action.getIcon().loadDrawable(mContext),
521                         action.getTitle(), action.getTitle(), false), new Function0<>() {
522                     @Override
523                     public Unit invoke() {
524                         try {
525                             action.getActionIntent().send();
526                             onFinish.run();
527                         } catch (PendingIntent.CanceledException e) {
528                             Log.e(TAG, "Failed to send intent");
529                         }
530                         return null;
531                     }
532                 }));
533 
534         return chip;
535     }
536 
constructActionChip(RemoteAction action, Runnable onFinish)537     private OverlayActionChip constructActionChip(RemoteAction action, Runnable onFinish) {
538         OverlayActionChip chip = (OverlayActionChip) LayoutInflater.from(mContext).inflate(
539                 R.layout.overlay_action_chip, mActionContainer, false);
540         chip.setText(action.getTitle());
541         chip.setContentDescription(action.getTitle());
542         chip.setIcon(action.getIcon(), false);
543         chip.setPendingIntent(action.getActionIntent(), onFinish);
544         chip.setAlpha(1);
545         return chip;
546     }
547 
updateTextSize(CharSequence text, TextView textView)548     private static void updateTextSize(CharSequence text, TextView textView) {
549         Paint paint = new Paint(textView.getPaint());
550         Resources res = textView.getResources();
551         float minFontSize = res.getDimensionPixelSize(R.dimen.clipboard_overlay_min_font);
552         float maxFontSize = res.getDimensionPixelSize(R.dimen.clipboard_overlay_max_font);
553         if (isOneWord(text) && fitsInView(text, textView, paint, minFontSize)) {
554             // If the text is a single word and would fit within the TextView at the min font size,
555             // find the biggest font size that will fit.
556             float fontSizePx = minFontSize;
557             while (fontSizePx + FONT_SEARCH_STEP_PX < maxFontSize
558                     && fitsInView(text, textView, paint, fontSizePx + FONT_SEARCH_STEP_PX)) {
559                 fontSizePx += FONT_SEARCH_STEP_PX;
560             }
561             // Need to turn off autosizing, otherwise setTextSize is a no-op.
562             textView.setAutoSizeTextTypeWithDefaults(TextView.AUTO_SIZE_TEXT_TYPE_NONE);
563             // It's possible to hit the max font size and not fill the width, so centering
564             // horizontally looks better in this case.
565             textView.setGravity(Gravity.CENTER);
566             textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, (int) fontSizePx);
567         } else {
568             // Otherwise just stick with autosize.
569             textView.setAutoSizeTextTypeUniformWithConfiguration((int) minFontSize,
570                     (int) maxFontSize, FONT_SEARCH_STEP_PX, TypedValue.COMPLEX_UNIT_PX);
571             textView.setGravity(Gravity.CENTER_VERTICAL | Gravity.START);
572         }
573     }
574 
fitsInView(CharSequence text, TextView textView, Paint paint, float fontSizePx)575     private static boolean fitsInView(CharSequence text, TextView textView, Paint paint,
576             float fontSizePx) {
577         paint.setTextSize(fontSizePx);
578         float size = paint.measureText(text.toString());
579         float availableWidth = textView.getWidth() - textView.getPaddingLeft()
580                 - textView.getPaddingRight();
581         return size < availableWidth;
582     }
583 
isOneWord(CharSequence text)584     private static boolean isOneWord(CharSequence text) {
585         return text.toString().split("\\s+", 2).length == 1;
586     }
587 
computeMargins(WindowInsets insets, int orientation)588     private static Rect computeMargins(WindowInsets insets, int orientation) {
589         DisplayCutout cutout = insets.getDisplayCutout();
590         Insets navBarInsets = insets.getInsets(WindowInsets.Type.navigationBars());
591         Insets imeInsets = insets.getInsets(WindowInsets.Type.ime());
592         if (cutout == null) {
593             return new Rect(0, 0, 0, Math.max(imeInsets.bottom, navBarInsets.bottom));
594         } else {
595             Insets waterfall = cutout.getWaterfallInsets();
596             if (orientation == ORIENTATION_PORTRAIT) {
597                 return new Rect(
598                         waterfall.left,
599                         Math.max(cutout.getSafeInsetTop(), waterfall.top),
600                         waterfall.right,
601                         Math.max(imeInsets.bottom,
602                                 Math.max(cutout.getSafeInsetBottom(),
603                                         Math.max(navBarInsets.bottom, waterfall.bottom))));
604             } else {
605                 return new Rect(
606                         waterfall.left,
607                         waterfall.top,
608                         waterfall.right,
609                         Math.max(imeInsets.bottom,
610                                 Math.max(navBarInsets.bottom, waterfall.bottom)));
611             }
612         }
613     }
614 }
615