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 androidx.core.widget;
18 
19 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
20 
21 import android.annotation.TargetApi;
22 import android.app.Activity;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.content.pm.PackageManager;
26 import android.content.pm.ResolveInfo;
27 import android.graphics.Paint;
28 import android.graphics.drawable.Drawable;
29 import android.os.Build;
30 import android.text.Editable;
31 import android.util.Log;
32 import android.util.TypedValue;
33 import android.view.ActionMode;
34 import android.view.Menu;
35 import android.view.MenuItem;
36 import android.view.View;
37 import android.widget.TextView;
38 
39 import androidx.annotation.DrawableRes;
40 import androidx.annotation.IntDef;
41 import androidx.annotation.IntRange;
42 import androidx.annotation.NonNull;
43 import androidx.annotation.Nullable;
44 import androidx.annotation.Px;
45 import androidx.annotation.RestrictTo;
46 import androidx.annotation.StyleRes;
47 import androidx.core.os.BuildCompat;
48 import androidx.core.util.Preconditions;
49 
50 import java.lang.annotation.Retention;
51 import java.lang.annotation.RetentionPolicy;
52 import java.lang.reflect.Field;
53 import java.lang.reflect.InvocationTargetException;
54 import java.lang.reflect.Method;
55 import java.util.ArrayList;
56 import java.util.List;
57 
58 /**
59  * Helper for accessing features in {@link TextView}.
60  */
61 public final class TextViewCompat {
62     private static final String LOG_TAG = "TextViewCompat";
63 
64     /**
65      * The TextView does not auto-size text (default).
66      */
67     public static final int AUTO_SIZE_TEXT_TYPE_NONE = TextView.AUTO_SIZE_TEXT_TYPE_NONE;
68 
69     /**
70      * The TextView scales text size both horizontally and vertically to fit within the
71      * container.
72      */
73     public static final int AUTO_SIZE_TEXT_TYPE_UNIFORM = TextView.AUTO_SIZE_TEXT_TYPE_UNIFORM;
74 
75     /** @hide */
76     @RestrictTo(LIBRARY_GROUP)
77     @IntDef({AUTO_SIZE_TEXT_TYPE_NONE, AUTO_SIZE_TEXT_TYPE_UNIFORM})
78     @Retention(RetentionPolicy.SOURCE)
79     public @interface AutoSizeTextType {}
80 
81     private static Field sMaximumField;
82     private static boolean sMaximumFieldFetched;
83     private static Field sMaxModeField;
84     private static boolean sMaxModeFieldFetched;
85 
86     private static Field sMinimumField;
87     private static boolean sMinimumFieldFetched;
88     private static Field sMinModeField;
89     private static boolean sMinModeFieldFetched;
90 
91     private static final int LINES = 1;
92 
93     // Hide constructor
TextViewCompat()94     private TextViewCompat() {}
95 
retrieveField(String fieldName)96     private static Field retrieveField(String fieldName) {
97         Field field = null;
98         try {
99             field = TextView.class.getDeclaredField(fieldName);
100             field.setAccessible(true);
101         } catch (NoSuchFieldException e) {
102             Log.e(LOG_TAG, "Could not retrieve " + fieldName + " field.");
103         }
104         return field;
105     }
106 
retrieveIntFromField(Field field, TextView textView)107     private static int retrieveIntFromField(Field field, TextView textView) {
108         try {
109             return field.getInt(textView);
110         } catch (IllegalAccessException e) {
111             Log.d(LOG_TAG, "Could not retrieve value of " + field.getName() + " field.");
112         }
113         return -1;
114     }
115 
116     /**
117      * Sets the Drawables (if any) to appear to the start of, above, to the end
118      * of, and below the text. Use {@code null} if you do not want a Drawable
119      * there. The Drawables must already have had {@link Drawable#setBounds}
120      * called.
121      * <p/>
122      * Calling this method will overwrite any Drawables previously set using
123      * {@link TextView#setCompoundDrawables} or related methods.
124      *
125      * @param textView The TextView against which to invoke the method.
126      * @attr name android:drawableStart
127      * @attr name android:drawableTop
128      * @attr name android:drawableEnd
129      * @attr name android:drawableBottom
130      */
setCompoundDrawablesRelative(@onNull TextView textView, @Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end, @Nullable Drawable bottom)131     public static void setCompoundDrawablesRelative(@NonNull TextView textView,
132             @Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end,
133             @Nullable Drawable bottom) {
134         if (Build.VERSION.SDK_INT >= 18) {
135             textView.setCompoundDrawablesRelative(start, top, end, bottom);
136         } else if (Build.VERSION.SDK_INT >= 17) {
137             boolean rtl = textView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
138             textView.setCompoundDrawables(rtl ? end : start, top, rtl ? start : end, bottom);
139         } else {
140             textView.setCompoundDrawables(start, top, end, bottom);
141         }
142     }
143 
144     /**
145      * Sets the Drawables (if any) to appear to the start of, above, to the end
146      * of, and below the text. Use {@code null} if you do not want a Drawable
147      * there. The Drawables' bounds will be set to their intrinsic bounds.
148      * <p/>
149      * Calling this method will overwrite any Drawables previously set using
150      * {@link TextView#setCompoundDrawables} or related methods.
151      *
152      * @param textView The TextView against which to invoke the method.
153      * @attr name android:drawableStart
154      * @attr name android:drawableTop
155      * @attr name android:drawableEnd
156      * @attr name android:drawableBottom
157      */
setCompoundDrawablesRelativeWithIntrinsicBounds(@onNull TextView textView, @Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end, @Nullable Drawable bottom)158     public static void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView,
159             @Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end,
160             @Nullable Drawable bottom) {
161         if (Build.VERSION.SDK_INT >= 18) {
162             textView.setCompoundDrawablesRelativeWithIntrinsicBounds(start, top, end, bottom);
163         } else if (Build.VERSION.SDK_INT >= 17) {
164             boolean rtl = textView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
165             textView.setCompoundDrawablesWithIntrinsicBounds(rtl ? end : start, top,
166                     rtl ? start : end,  bottom);
167         } else {
168             textView.setCompoundDrawablesWithIntrinsicBounds(start, top, end, bottom);
169         }
170     }
171 
172     /**
173      * Sets the Drawables (if any) to appear to the start of, above, to the end
174      * of, and below the text. Use 0 if you do not want a Drawable there. The
175      * Drawables' bounds will be set to their intrinsic bounds.
176      * <p/>
177      * Calling this method will overwrite any Drawables previously set using
178      * {@link TextView#setCompoundDrawables} or related methods.
179      *
180      * @param textView The TextView against which to invoke the method.
181      * @param start    Resource identifier of the start Drawable.
182      * @param top      Resource identifier of the top Drawable.
183      * @param end      Resource identifier of the end Drawable.
184      * @param bottom   Resource identifier of the bottom Drawable.
185      * @attr name android:drawableStart
186      * @attr name android:drawableTop
187      * @attr name android:drawableEnd
188      * @attr name android:drawableBottom
189      */
setCompoundDrawablesRelativeWithIntrinsicBounds(@onNull TextView textView, @DrawableRes int start, @DrawableRes int top, @DrawableRes int end, @DrawableRes int bottom)190     public static void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView,
191             @DrawableRes int start, @DrawableRes int top, @DrawableRes int end,
192             @DrawableRes int bottom) {
193         if (Build.VERSION.SDK_INT >= 18) {
194             textView.setCompoundDrawablesRelativeWithIntrinsicBounds(start, top, end, bottom);
195         } else if (Build.VERSION.SDK_INT >= 17) {
196             boolean rtl = textView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
197             textView.setCompoundDrawablesWithIntrinsicBounds(rtl ? end : start, top,
198                     rtl ? start : end, bottom);
199         } else {
200             textView.setCompoundDrawablesWithIntrinsicBounds(start, top, end, bottom);
201         }
202     }
203 
204     /**
205      * Returns the maximum number of lines displayed in the given TextView, or -1 if the maximum
206      * height was set in pixels instead.
207      */
getMaxLines(@onNull TextView textView)208     public static int getMaxLines(@NonNull TextView textView) {
209         if (Build.VERSION.SDK_INT >= 16) {
210             return textView.getMaxLines();
211         }
212 
213         if (!sMaxModeFieldFetched) {
214             sMaxModeField = retrieveField("mMaxMode");
215             sMaxModeFieldFetched = true;
216         }
217         if (sMaxModeField != null && retrieveIntFromField(sMaxModeField, textView) == LINES) {
218             // If the max mode is using lines, we can grab the maximum value
219             if (!sMaximumFieldFetched) {
220                 sMaximumField = retrieveField("mMaximum");
221                 sMaximumFieldFetched = true;
222             }
223             if (sMaximumField != null) {
224                 return retrieveIntFromField(sMaximumField, textView);
225             }
226         }
227         return -1;
228     }
229 
230     /**
231      * Returns the minimum number of lines displayed in the given TextView, or -1 if the minimum
232      * height was set in pixels instead.
233      */
getMinLines(@onNull TextView textView)234     public static int getMinLines(@NonNull TextView textView) {
235         if (Build.VERSION.SDK_INT >= 16) {
236             return textView.getMinLines();
237         }
238 
239         if (!sMinModeFieldFetched) {
240             sMinModeField = retrieveField("mMinMode");
241             sMinModeFieldFetched = true;
242         }
243         if (sMinModeField != null && retrieveIntFromField(sMinModeField, textView) == LINES) {
244             // If the min mode is using lines, we can grab the maximum value
245             if (!sMinimumFieldFetched) {
246                 sMinimumField = retrieveField("mMinimum");
247                 sMinimumFieldFetched = true;
248             }
249             if (sMinimumField != null) {
250                 return retrieveIntFromField(sMinimumField, textView);
251             }
252         }
253         return -1;
254     }
255 
256     /**
257      * Sets the text appearance from the specified style resource.
258      * <p>
259      * Use a framework-defined {@code TextAppearance} style like
260      * {@link android.R.style#TextAppearance_Material_Body1 @android:style/TextAppearance.Material.Body1}.
261      *
262      * @param textView The TextView against which to invoke the method.
263      * @param resId    The resource identifier of the style to apply.
264      */
setTextAppearance(@onNull TextView textView, @StyleRes int resId)265     public static void setTextAppearance(@NonNull TextView textView, @StyleRes int resId) {
266         if (Build.VERSION.SDK_INT >= 23) {
267             textView.setTextAppearance(resId);
268         } else {
269             textView.setTextAppearance(textView.getContext(), resId);
270         }
271     }
272 
273     /**
274      * Returns drawables for the start, top, end, and bottom borders from the given text view.
275      */
276     @NonNull
getCompoundDrawablesRelative(@onNull TextView textView)277     public static Drawable[] getCompoundDrawablesRelative(@NonNull TextView textView) {
278         if (Build.VERSION.SDK_INT >= 18) {
279             return textView.getCompoundDrawablesRelative();
280         }
281         if (Build.VERSION.SDK_INT >= 17) {
282             final boolean rtl = textView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
283             final Drawable[] compounds = textView.getCompoundDrawables();
284             if (rtl) {
285                 // If we're on RTL, we need to invert the horizontal result like above
286                 final Drawable start = compounds[2];
287                 final Drawable end = compounds[0];
288                 compounds[0] = start;
289                 compounds[2] = end;
290             }
291             return compounds;
292         }
293         return textView.getCompoundDrawables();
294     }
295 
296     /**
297      * Specify whether this widget should automatically scale the text to try to perfectly fit
298      * within the layout bounds by using the default auto-size configuration.
299      *
300      * @param autoSizeTextType the type of auto-size. Must be one of
301      *        {@link TextViewCompat#AUTO_SIZE_TEXT_TYPE_NONE} or
302      *        {@link TextViewCompat#AUTO_SIZE_TEXT_TYPE_UNIFORM}
303      *
304      * @attr name android:autoSizeTextType
305      */
306     @SuppressWarnings("RedundantCast") // Intentionally invoking interface method.
setAutoSizeTextTypeWithDefaults(@onNull TextView textView, int autoSizeTextType)307     public static void setAutoSizeTextTypeWithDefaults(@NonNull TextView textView,
308             int autoSizeTextType) {
309         if (Build.VERSION.SDK_INT >= 27) {
310             textView.setAutoSizeTextTypeWithDefaults(autoSizeTextType);
311         } else if (textView instanceof AutoSizeableTextView) {
312             ((AutoSizeableTextView) textView).setAutoSizeTextTypeWithDefaults(autoSizeTextType);
313         }
314     }
315 
316     /**
317      * Specify whether this widget should automatically scale the text to try to perfectly fit
318      * within the layout bounds. If all the configuration params are valid the type of auto-size is
319      * set to {@link TextViewCompat#AUTO_SIZE_TEXT_TYPE_UNIFORM}.
320      *
321      * @param autoSizeMinTextSize the minimum text size available for auto-size
322      * @param autoSizeMaxTextSize the maximum text size available for auto-size
323      * @param autoSizeStepGranularity the auto-size step granularity. It is used in conjunction with
324      *                                the minimum and maximum text size in order to build the set of
325      *                                text sizes the system uses to choose from when auto-sizing
326      * @param unit the desired dimension unit for all sizes above. See {@link TypedValue} for the
327      *             possible dimension units
328      *
329      * @throws IllegalArgumentException if any of the configuration params are invalid.
330      *
331      * @attr name android:autoSizeTextType
332      * @attr name android:autoSizeTextType
333      * @attr name android:autoSizeMinTextSize
334      * @attr name android:autoSizeMaxTextSize
335      * @attr name android:autoSizeStepGranularity
336      */
337     @SuppressWarnings("RedundantCast") // Intentionally invoking interface method.
setAutoSizeTextTypeUniformWithConfiguration( @onNull TextView textView, int autoSizeMinTextSize, int autoSizeMaxTextSize, int autoSizeStepGranularity, int unit)338     public static void setAutoSizeTextTypeUniformWithConfiguration(
339             @NonNull TextView textView,
340             int autoSizeMinTextSize,
341             int autoSizeMaxTextSize,
342             int autoSizeStepGranularity,
343             int unit) throws IllegalArgumentException {
344         if (Build.VERSION.SDK_INT >= 27) {
345             textView.setAutoSizeTextTypeUniformWithConfiguration(
346                     autoSizeMinTextSize, autoSizeMaxTextSize, autoSizeStepGranularity, unit);
347         } else if (textView instanceof AutoSizeableTextView) {
348             ((AutoSizeableTextView) textView).setAutoSizeTextTypeUniformWithConfiguration(
349                     autoSizeMinTextSize, autoSizeMaxTextSize, autoSizeStepGranularity, unit);
350         }
351     }
352 
353     /**
354      * Specify whether this widget should automatically scale the text to try to perfectly fit
355      * within the layout bounds. If at least one value from the <code>presetSizes</code> is valid
356      * then the type of auto-size is set to {@link TextViewCompat#AUTO_SIZE_TEXT_TYPE_UNIFORM}.
357      *
358      * @param presetSizes an {@code int} array of sizes in pixels
359      * @param unit the desired dimension unit for the preset sizes above. See {@link TypedValue} for
360      *             the possible dimension units
361      *
362      * @throws IllegalArgumentException if all of the <code>presetSizes</code> are invalid.
363      *_
364      * @attr name android:autoSizeTextType
365      * @attr name android:autoSizePresetSizes
366      */
367     @SuppressWarnings("RedundantCast") // Intentionally invoking interface method.
setAutoSizeTextTypeUniformWithPresetSizes(@onNull TextView textView, @NonNull int[] presetSizes, int unit)368     public static void setAutoSizeTextTypeUniformWithPresetSizes(@NonNull TextView textView,
369             @NonNull int[] presetSizes, int unit) throws IllegalArgumentException {
370         if (Build.VERSION.SDK_INT >= 27) {
371             textView.setAutoSizeTextTypeUniformWithPresetSizes(presetSizes, unit);
372         } else if (textView instanceof AutoSizeableTextView) {
373             ((AutoSizeableTextView) textView).setAutoSizeTextTypeUniformWithPresetSizes(
374                     presetSizes, unit);
375         }
376     }
377 
378     /**
379      * Returns the type of auto-size set for this widget.
380      *
381      * @return an {@code int} corresponding to one of the auto-size types:
382      *         {@link TextViewCompat#AUTO_SIZE_TEXT_TYPE_NONE} or
383      *         {@link TextViewCompat#AUTO_SIZE_TEXT_TYPE_UNIFORM}
384      *
385      * @attr name android:autoSizeTextType
386      */
387     @SuppressWarnings("RedundantCast") // Intentionally invoking interface method.
getAutoSizeTextType(@onNull TextView textView)388     public static int getAutoSizeTextType(@NonNull TextView textView) {
389         if (Build.VERSION.SDK_INT >= 27) {
390             return textView.getAutoSizeTextType();
391         }
392         if (textView instanceof AutoSizeableTextView) {
393             return ((AutoSizeableTextView) textView).getAutoSizeTextType();
394         }
395         return AUTO_SIZE_TEXT_TYPE_NONE;
396     }
397 
398     /**
399      * @return the current auto-size step granularity in pixels.
400      *
401      * @attr name android:autoSizeStepGranularity
402      */
403     @SuppressWarnings("RedundantCast") // Intentionally invoking interface method.
getAutoSizeStepGranularity(@onNull TextView textView)404     public static int getAutoSizeStepGranularity(@NonNull TextView textView) {
405         if (Build.VERSION.SDK_INT >= 27) {
406             return textView.getAutoSizeStepGranularity();
407         }
408         if (textView instanceof AutoSizeableTextView) {
409             return ((AutoSizeableTextView) textView).getAutoSizeStepGranularity();
410         }
411         return -1;
412     }
413 
414     /**
415      * @return the current auto-size minimum text size in pixels (the default is 12sp). Note that
416      *         if auto-size has not been configured this function returns {@code -1}.
417      *
418      * @attr name android:autoSizeMinTextSize
419      */
420     @SuppressWarnings("RedundantCast") // Intentionally invoking interface method.
getAutoSizeMinTextSize(@onNull TextView textView)421     public static int getAutoSizeMinTextSize(@NonNull TextView textView) {
422         if (Build.VERSION.SDK_INT >= 27) {
423             return textView.getAutoSizeMinTextSize();
424         }
425         if (textView instanceof AutoSizeableTextView) {
426             return ((AutoSizeableTextView) textView).getAutoSizeMinTextSize();
427         }
428         return -1;
429     }
430 
431     /**
432      * @return the current auto-size maximum text size in pixels (the default is 112sp). Note that
433      *         if auto-size has not been configured this function returns {@code -1}.
434      *
435      * @attr name android:autoSizeMaxTextSize
436      */
437     @SuppressWarnings("RedundantCast") // Intentionally invoking interface method.
getAutoSizeMaxTextSize(@onNull TextView textView)438     public static int getAutoSizeMaxTextSize(@NonNull TextView textView) {
439         if (Build.VERSION.SDK_INT >= 27) {
440             return textView.getAutoSizeMaxTextSize();
441         }
442         if (textView instanceof AutoSizeableTextView) {
443             return ((AutoSizeableTextView) textView).getAutoSizeMaxTextSize();
444         }
445         return -1;
446     }
447 
448     /**
449      * @return the current auto-size {@code int} sizes array (in pixels).
450      *
451      * @attr name android:autoSizePresetSizes
452      */
453     @NonNull
454     @SuppressWarnings("RedundantCast") // Intentionally invoking interface method.
getAutoSizeTextAvailableSizes(@onNull TextView textView)455     public static int[] getAutoSizeTextAvailableSizes(@NonNull TextView textView) {
456         if (Build.VERSION.SDK_INT >= 27) {
457             return textView.getAutoSizeTextAvailableSizes();
458         }
459         if (textView instanceof AutoSizeableTextView) {
460             return ((AutoSizeableTextView) textView).getAutoSizeTextAvailableSizes();
461         }
462         return new int[0];
463     }
464 
465     /**
466      * Sets a selection action mode callback on a TextView.
467      *
468      * Also this method can be used to fix a bug in framework SDK 26. On these affected devices,
469      * the bug causes the menu containing the options for handling ACTION_PROCESS_TEXT after text
470      * selection to miss a number of items. This method can be used to fix this wrong behaviour for
471      * a text view, by passing any custom callback implementation. If no custom callback is desired,
472      * a no-op implementation should be provided.
473      *
474      * Note that, by default, the bug will only be fixed when the default floating toolbar menu
475      * implementation is used. If a custom implementation of {@link Menu} is provided, this should
476      * provide the method Menu#removeItemAt(int) which removes a menu item by its position,
477      * as given by Menu#getItem(int). Also, the following post condition should hold: a call
478      * to removeItemAt(i), should not modify the results of getItem(j) for any j < i. Intuitively,
479      * removing an element from the menu should behave as removing an element from a list.
480      * Note that this method does not exist in the {@link Menu} interface. However, it is required,
481      * and going to be called by reflection, in order to display the correct process text items in
482      * the menu.
483      *
484      * @param textView The TextView to set the action selection mode callback on.
485      * @param callback The action selection mode callback to set on textView.
486      */
setCustomSelectionActionModeCallback(@onNull final TextView textView, @NonNull final ActionMode.Callback callback)487     public static void setCustomSelectionActionModeCallback(@NonNull final TextView textView,
488                 @NonNull final ActionMode.Callback callback) {
489         if (Build.VERSION.SDK_INT < 26 || Build.VERSION.SDK_INT > 27) {
490             textView.setCustomSelectionActionModeCallback(callback);
491             return;
492         }
493 
494         // A bug in O and O_MR1 causes a number of options for handling the ACTION_PROCESS_TEXT
495         // intent after selection to not be displayed in the menu, although they should be.
496         // Here we fix this, by removing the menu items created by the framework code, and
497         // adding them (and the missing ones) back correctly.
498         textView.setCustomSelectionActionModeCallback(new OreoCallback(callback, textView));
499     }
500 
501     @TargetApi(26) // TODO was anonymous but https://issuetracker.google.com/issues/76458979
502     private static class OreoCallback implements ActionMode.Callback {
503         // This constant should be correlated with its definition in the
504         // android.widget.Editor class.
505         private static final int MENU_ITEM_ORDER_PROCESS_TEXT_INTENT_ACTIONS_START = 100;
506         private final ActionMode.Callback mCallback;
507         private final TextView mTextView;
508 
509         // References to the MenuBuilder class and its removeItemAt(int) method.
510         // Since in most cases the menu instance processed by this callback is going
511         // to be a MenuBuilder, we keep these references to avoid querying for them
512         // frequently by reflection in recomputeProcessTextMenuItems.
513         private Class mMenuBuilderClass;
514         private Method mMenuBuilderRemoveItemAtMethod;
515         private boolean mCanUseMenuBuilderReferences;
516         private boolean mInitializedMenuBuilderReferences;
517 
OreoCallback(ActionMode.Callback callback, TextView textView)518         OreoCallback(ActionMode.Callback callback, TextView textView) {
519             mCallback = callback;
520             mTextView = textView;
521             mInitializedMenuBuilderReferences = false;
522         }
523 
524         @Override
onCreateActionMode(ActionMode mode, Menu menu)525         public boolean onCreateActionMode(ActionMode mode, Menu menu) {
526             return mCallback.onCreateActionMode(mode, menu);
527         }
528 
529         @Override
onPrepareActionMode(ActionMode mode, Menu menu)530         public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
531             recomputeProcessTextMenuItems(menu);
532             return mCallback.onPrepareActionMode(mode, menu);
533         }
534 
535         @Override
onActionItemClicked(ActionMode mode, MenuItem item)536         public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
537             return mCallback.onActionItemClicked(mode, item);
538         }
539 
540         @Override
onDestroyActionMode(ActionMode mode)541         public void onDestroyActionMode(ActionMode mode) {
542             mCallback.onDestroyActionMode(mode);
543         }
544 
recomputeProcessTextMenuItems(final Menu menu)545         private void recomputeProcessTextMenuItems(final Menu menu) {
546             final Context context = mTextView.getContext();
547             final PackageManager packageManager = context.getPackageManager();
548 
549             if (!mInitializedMenuBuilderReferences) {
550                 mInitializedMenuBuilderReferences = true;
551                 try {
552                     mMenuBuilderClass =
553                             Class.forName("com.android.internal.view.menu.MenuBuilder");
554                     mMenuBuilderRemoveItemAtMethod = mMenuBuilderClass
555                             .getDeclaredMethod("removeItemAt", Integer.TYPE);
556                     mCanUseMenuBuilderReferences = true;
557                 } catch (ClassNotFoundException | NoSuchMethodException e) {
558                     mMenuBuilderClass = null;
559                     mMenuBuilderRemoveItemAtMethod = null;
560                     mCanUseMenuBuilderReferences = false;
561                 }
562             }
563             // Remove the menu items created for ACTION_PROCESS_TEXT handlers.
564             try {
565                 final Method removeItemAtMethod =
566                         (mCanUseMenuBuilderReferences && mMenuBuilderClass.isInstance(menu))
567                                 ? mMenuBuilderRemoveItemAtMethod
568                                 : menu.getClass()
569                                         .getDeclaredMethod("removeItemAt", Integer.TYPE);
570                 for (int i = menu.size() - 1; i >= 0; --i) {
571                     final MenuItem item = menu.getItem(i);
572                     if (item.getIntent() != null && Intent.ACTION_PROCESS_TEXT
573                             .equals(item.getIntent().getAction())) {
574                         removeItemAtMethod.invoke(menu, i);
575                     }
576                 }
577             } catch (NoSuchMethodException | IllegalAccessException
578                     | InvocationTargetException e) {
579                 // There is a menu custom implementation used which is not providing
580                 // a removeItemAt(int) menu. There is nothing we can do in this case.
581                 return;
582             }
583 
584             // Populate the menu again with the ACTION_PROCESS_TEXT handlers.
585             final List<ResolveInfo> supportedActivities =
586                     getSupportedActivities(context, packageManager);
587             for (int i = 0; i < supportedActivities.size(); ++i) {
588                 final ResolveInfo info = supportedActivities.get(i);
589                 menu.add(Menu.NONE, Menu.NONE,
590                         MENU_ITEM_ORDER_PROCESS_TEXT_INTENT_ACTIONS_START + i,
591                         info.loadLabel(packageManager))
592                         .setIntent(createProcessTextIntentForResolveInfo(info, mTextView))
593                         .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
594             }
595         }
596 
getSupportedActivities(final Context context, final PackageManager packageManager)597         private List<ResolveInfo> getSupportedActivities(final Context context,
598                 final PackageManager packageManager) {
599             final List<ResolveInfo> supportedActivities = new ArrayList<>();
600             boolean canStartActivityForResult = context instanceof Activity;
601             if (!canStartActivityForResult) {
602                 return supportedActivities;
603             }
604             final List<ResolveInfo> unfiltered =
605                     packageManager.queryIntentActivities(createProcessTextIntent(), 0);
606             for (ResolveInfo info : unfiltered) {
607                 if (isSupportedActivity(info, context)) {
608                     supportedActivities.add(info);
609                 }
610             }
611             return supportedActivities;
612         }
613 
isSupportedActivity(final ResolveInfo info, final Context context)614         private boolean isSupportedActivity(final ResolveInfo info, final Context context) {
615             if (context.getPackageName().equals(info.activityInfo.packageName)) {
616                 return true;
617             }
618             if (!info.activityInfo.exported) {
619                 return false;
620             }
621             return info.activityInfo.permission == null
622                     || context.checkSelfPermission(info.activityInfo.permission)
623                         == PackageManager.PERMISSION_GRANTED;
624         }
625 
createProcessTextIntentForResolveInfo(final ResolveInfo info, final TextView textView11)626         private Intent createProcessTextIntentForResolveInfo(final ResolveInfo info,
627                 final TextView textView11) {
628             return createProcessTextIntent()
629                     .putExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, !isEditable(textView11))
630                     .setClassName(info.activityInfo.packageName, info.activityInfo.name);
631         }
632 
isEditable(final TextView textView11)633         private boolean isEditable(final TextView textView11) {
634             return textView11 instanceof Editable
635                     && textView11.onCheckIsTextEditor()
636                     && textView11.isEnabled();
637         }
638 
createProcessTextIntent()639         private Intent createProcessTextIntent() {
640             return new Intent().setAction(Intent.ACTION_PROCESS_TEXT).setType("text/plain");
641         }
642     }
643 
644     /**
645      * Updates the top padding of the TextView so that {@code firstBaselineToTopHeight} is
646      * equal to the distance between the first text baseline and the top of this TextView.
647      * <strong>Note</strong> that if {@code FontMetrics.top} or {@code FontMetrics.ascent} was
648      * already greater than {@code firstBaselineToTopHeight}, the top padding is not updated.
649      *
650      * @param firstBaselineToTopHeight distance between first baseline to top of the container
651      *      in pixels
652      *
653      * @see #getFirstBaselineToTopHeight(TextView)
654      * @see TextView#setPadding(int, int, int, int)
655      * @see TextView#setPaddingRelative(int, int, int, int)
656      *
657      * @attr name android:firstBaselineToTopHeight
658      */
setFirstBaselineToTopHeight( @onNull final TextView textView, @Px @IntRange(from = 0) final int firstBaselineToTopHeight)659     public static void setFirstBaselineToTopHeight(
660             @NonNull final TextView textView,
661             @Px @IntRange(from = 0) final int firstBaselineToTopHeight) {
662         Preconditions.checkArgumentNonnegative(firstBaselineToTopHeight);
663         if (BuildCompat.isAtLeastP()) {
664             textView.setFirstBaselineToTopHeight(firstBaselineToTopHeight);
665             return;
666         }
667 
668         final Paint.FontMetricsInt fontMetrics = textView.getPaint().getFontMetricsInt();
669         final int fontMetricsTop;
670         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN
671                 // The includeFontPadding attribute was introduced
672                 // in SDK16, and it is true by default.
673                 || textView.getIncludeFontPadding()) {
674             fontMetricsTop = fontMetrics.top;
675         } else {
676             fontMetricsTop = fontMetrics.ascent;
677         }
678 
679         // TODO: Decide if we want to ignore density ratio (i.e. when the user changes font size
680         // in settings). At the moment, we don't.
681 
682         if (firstBaselineToTopHeight > Math.abs(fontMetricsTop)) {
683             final int paddingTop = firstBaselineToTopHeight - (-fontMetricsTop);
684             textView.setPadding(textView.getPaddingLeft(), paddingTop,
685                     textView.getPaddingRight(), textView.getPaddingBottom());
686         }
687     }
688 
689     /**
690      * Updates the bottom padding of the TextView so that {@code lastBaselineToBottomHeight} is
691      * equal to the distance between the last text baseline and the bottom of this TextView.
692      * <strong>Note</strong> that if {@code FontMetrics.bottom} or {@code FontMetrics.descent} was
693      * already greater than {@code lastBaselineToBottomHeight}, the bottom padding is not updated.
694      *
695      * @param lastBaselineToBottomHeight distance between last baseline to bottom of the container
696      *      in pixels
697      *
698      * @see #getLastBaselineToBottomHeight(TextView)
699      * @see TextView#setPadding(int, int, int, int)
700      * @see TextView#setPaddingRelative(int, int, int, int)
701      *
702      * @attr name android:lastBaselineToBottomHeight
703      */
setLastBaselineToBottomHeight( @onNull final TextView textView, @Px @IntRange(from = 0) int lastBaselineToBottomHeight)704     public static void setLastBaselineToBottomHeight(
705             @NonNull final TextView textView,
706             @Px @IntRange(from = 0) int lastBaselineToBottomHeight) {
707         Preconditions.checkArgumentNonnegative(lastBaselineToBottomHeight);
708 
709         final Paint.FontMetricsInt fontMetrics = textView.getPaint().getFontMetricsInt();
710         final int fontMetricsBottom;
711         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN
712                 // The includeFontPadding attribute was introduced
713                 // in SDK16, and it is true by default.
714                 || textView.getIncludeFontPadding()) {
715             fontMetricsBottom = fontMetrics.bottom;
716         } else {
717             fontMetricsBottom = fontMetrics.descent;
718         }
719 
720         // TODO: Decide if we want to ignore density ratio (i.e. when the user changes font size
721         // in settings). At the moment, we don't.
722 
723         if (lastBaselineToBottomHeight > Math.abs(fontMetricsBottom)) {
724             final int paddingBottom = lastBaselineToBottomHeight - fontMetricsBottom;
725             textView.setPadding(textView.getPaddingLeft(), textView.getPaddingTop(),
726                     textView.getPaddingRight(), paddingBottom);
727         }
728     }
729 
730     /**
731      * Returns the distance between the first text baseline and the top of this TextView.
732      *
733      * @see #setFirstBaselineToTopHeight(TextView, int)
734      * @attr name android:firstBaselineToTopHeight
735      */
getFirstBaselineToTopHeight(@onNull final TextView textView)736     public static int getFirstBaselineToTopHeight(@NonNull final TextView textView) {
737         return textView.getPaddingTop() - textView.getPaint().getFontMetricsInt().top;
738     }
739 
740     /**
741      * Returns the distance between the last text baseline and the bottom of this TextView.
742      *
743      * @see #setLastBaselineToBottomHeight(TextView, int)
744      * @attr name android:lastBaselineToBottomHeight
745      */
getLastBaselineToBottomHeight(@onNull final TextView textView)746     public static int getLastBaselineToBottomHeight(@NonNull final TextView textView) {
747         return textView.getPaddingBottom() + textView.getPaint().getFontMetricsInt().bottom;
748     }
749 
750 
751     /**
752      * Sets an explicit line height for this TextView. This is equivalent to the vertical distance
753      * between subsequent baselines in the TextView.
754      *
755      * @param lineHeight the line height in pixels
756      *
757      * @see TextView#setLineSpacing(float, float)
758      * @see TextView#getLineSpacingExtra()
759      * @see TextView#getLineSpacingMultiplier()
760      *
761      * @attr name android:lineHeight
762      */
setLineHeight(@onNull final TextView textView, @Px @IntRange(from = 0) int lineHeight)763     public static void setLineHeight(@NonNull final TextView textView,
764                               @Px @IntRange(from = 0) int lineHeight) {
765         Preconditions.checkArgumentNonnegative(lineHeight);
766 
767         final int fontHeight = textView.getPaint().getFontMetricsInt(null);
768         // Make sure we don't setLineSpacing if it's not needed to avoid unnecessary redraw.
769         if (lineHeight != fontHeight) {
770             // Set lineSpacingExtra by the difference of lineSpacing with lineHeight
771             textView.setLineSpacing(lineHeight - fontHeight, 1f);
772         }
773     }
774 }
775