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