diff options
author | Justin Klaassen <justinklaassen@google.com> | 2018-04-03 23:21:57 -0400 |
---|---|---|
committer | Justin Klaassen <justinklaassen@google.com> | 2018-04-03 23:21:57 -0400 |
commit | 4d01eeaffaa720e4458a118baa137a11614f00f7 (patch) | |
tree | 66751893566986236788e3c796a7cc5e90d05f52 /android/widget | |
parent | a192cc2a132cb0ee8588e2df755563ec7008c179 (diff) | |
download | android-28-4d01eeaffaa720e4458a118baa137a11614f00f7.tar.gz |
Import Android SDK Platform P [4697573]
/google/data/ro/projects/android/fetch_artifact \
--bid 4697573 \
--target sdk_phone_armv7-win_sdk \
sdk-repo-linux-sources-4697573.zip
AndroidVersion.ApiLevel has been modified to appear as 28
Change-Id: If80578c3c657366cc9cf75f8db13d46e2dd4e077
Diffstat (limited to 'android/widget')
24 files changed, 2233 insertions, 1112 deletions
diff --git a/android/widget/AbsListView.java b/android/widget/AbsListView.java index 594d2400..298c61e4 100644 --- a/android/widget/AbsListView.java +++ b/android/widget/AbsListView.java @@ -31,7 +31,6 @@ import android.graphics.drawable.TransitionDrawable; import android.os.Bundle; import android.os.Debug; import android.os.Handler; -import android.os.LocaleList; import android.os.Parcel; import android.os.Parcelable; import android.os.StrictMode; @@ -90,7 +89,7 @@ import java.util.List; /** * Base class that can be used to implement virtualized lists of items. A list does - * not have a spatial definition here. For instance, subclases of this class can + * not have a spatial definition here. For instance, subclasses of this class can * display the content of the list in a grid, in a carousel, as stack, etc. * * @attr ref android.R.styleable#AbsListView_listSelector @@ -6036,11 +6035,6 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te public boolean commitContent(InputContentInfo inputContentInfo, int flags, Bundle opts) { return getTarget().commitContent(inputContentInfo, flags, opts); } - - @Override - public void reportLanguageHint(@NonNull LocaleList languageHint) { - getTarget().reportLanguageHint(languageHint); - } } /** @@ -6864,7 +6858,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te // detached and we do not allow detached views to fire accessibility // events. So we are announcing that the subtree changed giving a chance // to clients holding on to a view in this subtree to refresh it. - notifyAccessibilityStateChanged( + notifyViewAccessibilityStateChangedIfNeeded( AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE); // Don't scrap views that have transient state. diff --git a/android/widget/AbsSeekBar.java b/android/widget/AbsSeekBar.java index 1d1fcc96..61a58733 100644 --- a/android/widget/AbsSeekBar.java +++ b/android/widget/AbsSeekBar.java @@ -863,7 +863,7 @@ public abstract class AbsSeekBar extends ProgressBar { } final int range = getMax() - getMin(); - progress += scale * range; + progress += scale * range + getMin(); setHotspot(x, y); setProgressInternal(Math.round(progress), true, false); diff --git a/android/widget/AdapterView.java b/android/widget/AdapterView.java index 08374cb1..6c192563 100644 --- a/android/widget/AdapterView.java +++ b/android/widget/AdapterView.java @@ -1093,7 +1093,7 @@ public abstract class AdapterView<T extends Adapter> extends ViewGroup { checkSelectionChanged(); } - notifyAccessibilitySubtreeChanged(); + notifySubtreeAccessibilityStateChangedIfNeeded(); } /** diff --git a/android/widget/CheckedTextView.java b/android/widget/CheckedTextView.java index af01a3eb..92bfd56d 100644 --- a/android/widget/CheckedTextView.java +++ b/android/widget/CheckedTextView.java @@ -132,7 +132,7 @@ public class CheckedTextView extends TextView implements Checkable { if (mChecked != checked) { mChecked = checked; refreshDrawableState(); - notifyAccessibilityStateChanged( + notifyViewAccessibilityStateChangedIfNeeded( AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED); } } diff --git a/android/widget/CompoundButton.java b/android/widget/CompoundButton.java index e57f1536..0762b156 100644 --- a/android/widget/CompoundButton.java +++ b/android/widget/CompoundButton.java @@ -158,7 +158,7 @@ public abstract class CompoundButton extends Button implements Checkable { mCheckedFromResource = false; mChecked = checked; refreshDrawableState(); - notifyAccessibilityStateChanged( + notifyViewAccessibilityStateChangedIfNeeded( AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED); // Avoid infinite recursions if setChecked() is called from a listener diff --git a/android/widget/Editor.java b/android/widget/Editor.java index 7bb0db1c..99467265 100644 --- a/android/widget/Editor.java +++ b/android/widget/Editor.java @@ -17,11 +17,13 @@ package android.widget; import android.R; +import android.animation.ValueAnimator; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.PendingIntent; import android.app.PendingIntent.CanceledException; +import android.app.RemoteAction; import android.content.ClipData; import android.content.ClipData.Item; import android.content.Context; @@ -37,6 +39,7 @@ import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Path; +import android.graphics.PointF; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.ColorDrawable; @@ -99,6 +102,7 @@ import android.view.ViewGroup.LayoutParams; import android.view.ViewTreeObserver; import android.view.WindowManager; import android.view.accessibility.AccessibilityNodeInfo; +import android.view.animation.LinearInterpolator; import android.view.inputmethod.CorrectionInfo; import android.view.inputmethod.CursorAnchorInfo; import android.view.inputmethod.EditorInfo; @@ -107,7 +111,7 @@ import android.view.inputmethod.ExtractedTextRequest; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; import android.view.textclassifier.TextClassification; -import android.view.textclassifier.TextLinks; +import android.view.textclassifier.TextClassificationManager; import android.widget.AdapterView.OnItemClickListener; import android.widget.TextView.Drawables; import android.widget.TextView.OnEditorActionListener; @@ -200,11 +204,11 @@ public class Editor { private final boolean mHapticTextHandleEnabled; - private final Magnifier mMagnifier; + private final MagnifierMotionAnimator mMagnifierAnimator; private final Runnable mUpdateMagnifierRunnable = new Runnable() { @Override public void run() { - mMagnifier.update(); + mMagnifierAnimator.update(); } }; // Update the magnifier contents whenever anything in the view hierarchy is updated. @@ -215,7 +219,7 @@ public class Editor { new ViewTreeObserver.OnDrawListener() { @Override public void onDraw() { - if (mMagnifier != null) { + if (mMagnifierAnimator != null) { // Posting the method will ensure that updating the magnifier contents will // happen right after the rendering of the current frame. mTextView.post(mUpdateMagnifierRunnable); @@ -262,7 +266,8 @@ public class Editor { boolean mDiscardNextActionUp; boolean mIgnoreActionUpEvent; - long mShowCursor; + private long mShowCursor; + private boolean mRenderCursorRegardlessTiming; private Blink mBlink; boolean mCursorVisible = true; @@ -285,6 +290,7 @@ public class Editor { boolean mShowSoftInputOnFocus = true; private boolean mPreserveSelection; private boolean mRestartActionModeOnNextRefresh; + private boolean mRequestingLinkActionMode; private SelectionActionModeHelper mSelectionActionModeHelper; @@ -370,7 +376,9 @@ public class Editor { mHapticTextHandleEnabled = mTextView.getContext().getResources().getBoolean( com.android.internal.R.bool.config_enableHapticTextHandle); - mMagnifier = FLAG_USE_MAGNIFIER ? new Magnifier(mTextView) : null; + if (FLAG_USE_MAGNIFIER) { + mMagnifierAnimator = new MagnifierMotionAnimator(new Magnifier(mTextView)); + } } ParcelableParcel saveInstanceState() { @@ -681,11 +689,22 @@ public class Editor { } } - boolean isCursorVisible() { + private boolean isCursorVisible() { // The default value is true, even when there is no associated Editor return mCursorVisible && mTextView.isTextEditable(); } + boolean shouldRenderCursor() { + if (!isCursorVisible()) { + return false; + } + if (mRenderCursorRegardlessTiming) { + return true; + } + final long showCursorDelta = SystemClock.uptimeMillis() - mShowCursor; + return showCursorDelta % (2 * BLINK) < BLINK; + } + void prepareCursorControllers() { boolean windowSupportsHandles = false; @@ -1299,6 +1318,16 @@ public class Editor { if (mSelectionModifierCursorController != null) { mSelectionModifierCursorController.resetTouchOffsets(); } + + ensureNoSelectionIfNonSelectable(); + } + } + + private void ensureNoSelectionIfNonSelectable() { + // This could be the case if a TextLink has been tapped. + if (!mTextView.textCanBeSelected() && mTextView.hasSelection()) { + Selection.setSelection((Spannable) mTextView.getText(), + mTextView.length(), mTextView.length()); } } @@ -1382,6 +1411,8 @@ public class Editor { // Don't leave us in the middle of a batch edit. Same as in onFocusChanged ensureEndedBatchEdit(); + + ensureNoSelectionIfNonSelectable(); } } @@ -1745,16 +1776,19 @@ public class Editor { highlight = null; } + if (mSelectionActionModeHelper != null) { + mSelectionActionModeHelper.onDraw(canvas); + if (mSelectionActionModeHelper.isDrawingHighlight()) { + highlight = null; + } + } + if (mTextView.canHaveDisplayList() && canvas.isHardwareAccelerated()) { drawHardwareAccelerated(canvas, layout, highlight, highlightPaint, cursorOffsetVertical); } else { layout.draw(canvas, highlight, highlightPaint, cursorOffsetVertical); } - - if (mSelectionActionModeHelper != null) { - mSelectionActionModeHelper.onDraw(canvas); - } } private void drawHardwareAccelerated(Canvas canvas, Layout layout, Path highlight, @@ -2090,13 +2124,13 @@ public class Editor { getSelectionActionModeHelper().startSelectionActionModeAsync(adjustSelection); } - void startLinkActionModeAsync(TextLinks.TextLink link) { - Preconditions.checkNotNull(link); + void startLinkActionModeAsync(int start, int end) { if (!(mTextView.getText() instanceof Spannable)) { return; } stopTextActionMode(); - getSelectionActionModeHelper().startLinkActionModeAsync(link); + mRequestingLinkActionMode = true; + getSelectionActionModeHelper().startLinkActionModeAsync(start, end); } /** @@ -2181,7 +2215,9 @@ public class Editor { mTextActionMode = mTextView.startActionMode(actionModeCallback, ActionMode.TYPE_FLOATING); final boolean selectionStarted = mTextActionMode != null; - if (selectionStarted && !mTextView.isTextSelectable() && mShowSoftInputOnFocus) { + if (selectionStarted + && mTextView.isTextEditable() && !mTextView.isTextSelectable() + && mShowSoftInputOnFocus) { // Show the IME to be able to replace text, except when selecting non editable text. final InputMethodManager imm = InputMethodManager.peekInstance(); if (imm != null) { @@ -2291,10 +2327,14 @@ public class Editor { if (!selectAllGotFocus && text.length() > 0) { // Move cursor final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY()); - Selection.setSelection((Spannable) text, offset); - if (mSpellChecker != null) { - // When the cursor moves, the word that was typed may need spell check - mSpellChecker.onSelectionChanged(); + + final boolean shouldInsertCursor = !mRequestingLinkActionMode; + if (shouldInsertCursor) { + Selection.setSelection((Spannable) text, offset); + if (mSpellChecker != null) { + // When the cursor moves, the word that was typed may need spell check + mSpellChecker.onSelectionChanged(); + } } if (!extractedTextModeWillBeStarted()) { @@ -2304,16 +2344,17 @@ public class Editor { mTextView.removeCallbacks(mInsertionActionModeRunnable); } - mShowSuggestionRunnable = new Runnable() { - public void run() { - replace(); - } - }; + mShowSuggestionRunnable = this::replace; + // removeCallbacks is performed on every touch mTextView.postDelayed(mShowSuggestionRunnable, ViewConfiguration.getDoubleTapTimeout()); } else if (hasInsertionController()) { - getInsertionController().show(); + if (shouldInsertCursor) { + getInsertionController().show(); + } else { + getInsertionController().hide(); + } } } } @@ -3997,7 +4038,7 @@ public class Editor { private void updateAssistMenuItems(Menu menu) { clearAssistMenuItems(menu); - if (!mTextView.isDeviceProvisioned()) { + if (!shouldEnableAssistMenuItems()) { return; } final TextClassification textClassification = @@ -4005,39 +4046,44 @@ public class Editor { if (textClassification == null) { return; } - if (isValidAssistMenuItem( - textClassification.getIcon(), - textClassification.getLabel(), - textClassification.getIntent())) { + if (!textClassification.getActions().isEmpty()) { + // Primary assist action (Always shown). + final MenuItem item = addAssistMenuItem(menu, + textClassification.getActions().get(0), TextView.ID_ASSIST, + MENU_ITEM_ORDER_ASSIST, MenuItem.SHOW_AS_ACTION_ALWAYS); + item.setIntent(textClassification.getIntent()); + } else if (hasLegacyAssistItem(textClassification)) { + // Legacy primary assist action (Always shown). final MenuItem item = menu.add( TextView.ID_ASSIST, TextView.ID_ASSIST, MENU_ITEM_ORDER_ASSIST, textClassification.getLabel()) .setIcon(textClassification.getIcon()) .setIntent(textClassification.getIntent()); item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); - mAssistClickHandlers.put( - item, TextClassification.createStartActivityOnClickListener( - mTextView.getContext(), textClassification.getIntent())); - } - final int count = textClassification.getSecondaryActionsCount(); - for (int i = 0; i < count; i++) { - if (!isValidAssistMenuItem( - textClassification.getSecondaryIcon(i), - textClassification.getSecondaryLabel(i), - textClassification.getSecondaryIntent(i))) { - continue; - } - final int order = MENU_ITEM_ORDER_SECONDARY_ASSIST_ACTIONS_START + i; - final MenuItem item = menu.add( - TextView.ID_ASSIST, Menu.NONE, order, - textClassification.getSecondaryLabel(i)) - .setIcon(textClassification.getSecondaryIcon(i)) - .setIntent(textClassification.getSecondaryIntent(i)); - item.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); - mAssistClickHandlers.put(item, - TextClassification.createStartActivityOnClickListener( - mTextView.getContext(), textClassification.getSecondaryIntent(i))); + mAssistClickHandlers.put(item, TextClassification.createIntentOnClickListener( + TextClassification.createPendingIntent(mTextView.getContext(), + textClassification.getIntent()))); + } + final int count = textClassification.getActions().size(); + for (int i = 1; i < count; i++) { + // Secondary assist action (Never shown). + addAssistMenuItem(menu, textClassification.getActions().get(i), Menu.NONE, + MENU_ITEM_ORDER_SECONDARY_ASSIST_ACTIONS_START + i - 1, + MenuItem.SHOW_AS_ACTION_NEVER); + } + } + + private MenuItem addAssistMenuItem(Menu menu, RemoteAction action, int intemId, int order, + int showAsAction) { + final MenuItem item = menu.add(TextView.ID_ASSIST, intemId, order, action.getTitle()) + .setContentDescription(action.getContentDescription()); + if (action.shouldShowIcon()) { + item.setIcon(action.getIcon().loadDrawable(mTextView.getContext())); } + item.setShowAsAction(showAsAction); + mAssistClickHandlers.put(item, + TextClassification.createIntentOnClickListener(action.getActionIntent())); + return item; } private void clearAssistMenuItems(Menu menu) { @@ -4052,30 +4098,11 @@ public class Editor { } } - private boolean isValidAssistMenuItem(Drawable icon, CharSequence label, Intent intent) { - final boolean hasUi = icon != null || !TextUtils.isEmpty(label); - final boolean hasAction = isSupportedIntent(intent); - return hasUi && hasAction; - } - - private boolean isSupportedIntent(Intent intent) { - if (intent == null) { - return false; - } - final Context context = mTextView.getContext(); - final ResolveInfo info = context.getPackageManager().resolveActivity(intent, 0); - final boolean samePackage = context.getPackageName().equals( - info.activityInfo.packageName); - if (samePackage) { - return true; - } - - final boolean exported = info.activityInfo.exported; - final boolean requiresPermission = info.activityInfo.permission != null; - final boolean hasPermission = !requiresPermission - || context.checkSelfPermission(info.activityInfo.permission) - == PackageManager.PERMISSION_GRANTED; - return exported && hasPermission; + private boolean hasLegacyAssistItem(TextClassification classification) { + // Check whether we have the UI data and and action. + return (classification.getIcon() != null || !TextUtils.isEmpty( + classification.getLabel())) && (classification.getIntent() != null + || classification.getOnClickListener() != null); } private boolean onAssistMenuItemClicked(MenuItem assistMenuItem) { @@ -4083,7 +4110,7 @@ public class Editor { final TextClassification textClassification = getSelectionActionModeHelper().getTextClassification(); - if (!mTextView.isDeviceProvisioned() || textClassification == null) { + if (!shouldEnableAssistMenuItems() || textClassification == null) { // No textClassification result to handle the click. Eat the click. return true; } @@ -4092,8 +4119,8 @@ public class Editor { if (onClickListener == null) { final Intent intent = assistMenuItem.getIntent(); if (intent != null) { - onClickListener = TextClassification.createStartActivityOnClickListener( - mTextView.getContext(), intent); + onClickListener = TextClassification.createIntentOnClickListener( + TextClassification.createPendingIntent(mTextView.getContext(), intent)); } } if (onClickListener != null) { @@ -4104,6 +4131,12 @@ public class Editor { return true; } + private boolean shouldEnableAssistMenuItems() { + return mTextView.isDeviceProvisioned() + && TextClassificationManager.getSettings(mTextView.getContext()) + .isSmartTextShareEnabled(); + } + @Override public boolean onActionItemClicked(ActionMode mode, MenuItem item) { getSelectionActionModeHelper().onSelectionAction(item.getItemId()); @@ -4146,6 +4179,7 @@ public class Editor { } mAssistClickHandlers.clear(); + mRequestingLinkActionMode = false; } @Override @@ -4171,7 +4205,7 @@ public class Editor { primaryHorizontal, layout.getLineTop(line), primaryHorizontal, - layout.getLineBottom(line) - layout.getLineBottom(line) + mHandleHeight); + layout.getLineBottom(line) + mHandleHeight); } // Take TextView's padding and scroll into account. int textHorizontalOffset = mTextView.viewportToContentHorizontalOffset(); @@ -4290,6 +4324,88 @@ public class Editor { } } + private static class MagnifierMotionAnimator { + private static final long DURATION = 100 /* miliseconds */; + + // The magnifier being animated. + private final Magnifier mMagnifier; + // A value animator used to animate the magnifier. + private final ValueAnimator mAnimator; + + // Whether the magnifier is currently visible. + private boolean mMagnifierIsShowing; + // The coordinates of the magnifier when the currently running animation started. + private float mAnimationStartX; + private float mAnimationStartY; + // The coordinates of the magnifier in the latest animation frame. + private float mAnimationCurrentX; + private float mAnimationCurrentY; + // The latest coordinates the motion animator was asked to #show() the magnifier at. + private float mLastX; + private float mLastY; + + private MagnifierMotionAnimator(final Magnifier magnifier) { + mMagnifier = magnifier; + // Prepare the animator used to run the motion animation. + mAnimator = ValueAnimator.ofFloat(0, 1); + mAnimator.setDuration(DURATION); + mAnimator.setInterpolator(new LinearInterpolator()); + mAnimator.addUpdateListener((animation) -> { + // Interpolate to find the current position of the magnifier. + mAnimationCurrentX = mAnimationStartX + + (mLastX - mAnimationStartX) * animation.getAnimatedFraction(); + mAnimationCurrentY = mAnimationStartY + + (mLastY - mAnimationStartY) * animation.getAnimatedFraction(); + mMagnifier.show(mAnimationCurrentX, mAnimationCurrentY); + }); + } + + /** + * Shows the magnifier at a new position. + * If the y coordinate is different from the previous y coordinate + * (probably corresponding to a line jump in the text), a short + * animation is added to the jump. + */ + private void show(final float x, final float y) { + final boolean startNewAnimation = mMagnifierIsShowing && y != mLastY; + + if (startNewAnimation) { + if (mAnimator.isRunning()) { + mAnimator.cancel(); + mAnimationStartX = mAnimationCurrentX; + mAnimationStartY = mAnimationCurrentY; + } else { + mAnimationStartX = mLastX; + mAnimationStartY = mLastY; + } + mAnimator.start(); + } else { + if (!mAnimator.isRunning()) { + mMagnifier.show(x, y); + } + } + mLastX = x; + mLastY = y; + mMagnifierIsShowing = true; + } + + /** + * Updates the content of the magnifier. + */ + private void update() { + mMagnifier.update(); + } + + /** + * Dismisses the magnifier, or does nothing if it is already dismissed. + */ + private void dismiss() { + mMagnifier.dismiss(); + mAnimator.cancel(); + mMagnifierIsShowing = false; + } + } + @VisibleForTesting public abstract class HandleView extends View implements TextViewPositionListener { protected Drawable mDrawable; @@ -4469,8 +4585,8 @@ public class Editor { return mContainer.isShowing(); } - private boolean isVisible() { - // Always show a dragging handle. + private boolean shouldShow() { + // A dragging handle should always be shown. if (mIsDragging) { return true; } @@ -4483,6 +4599,10 @@ public class Editor { mPositionX + mHotspotX + getHorizontalOffset(), mPositionY); } + private void setVisible(final boolean visible) { + mContainer.getContentView().setVisibility(visible ? VISIBLE : INVISIBLE); + } + public abstract int getCurrentCursorOffset(); protected abstract void updateSelection(int offset); @@ -4576,7 +4696,7 @@ public class Editor { onHandleMoved(); } - if (isVisible()) { + if (shouldShow()) { // Transform to the window coordinates to follow the view tranformation. final int[] pts = { mPositionX + mHotspotX + getHorizontalOffset(), mPositionY}; mTextView.transformFromViewToWindowSpace(pts); @@ -4629,49 +4749,134 @@ public class Editor { return 0; } - protected final void showMagnifier(@NonNull final MotionEvent event) { - if (mMagnifier == null) { - return; - } + private boolean tooLargeTextForMagnifier() { + final float magnifierContentHeight = Math.round( + mMagnifierAnimator.mMagnifier.getHeight() + / mMagnifierAnimator.mMagnifier.getZoom()); + final Paint.FontMetrics fontMetrics = mTextView.getPaint().getFontMetrics(); + final float glyphHeight = fontMetrics.descent - fontMetrics.ascent; + return glyphHeight > magnifierContentHeight; + } + + /** + * Computes the position where the magnifier should be shown, relative to + * {@code mTextView}, and writes them to {@code showPosInView}. Also decides + * whether the magnifier should be shown or dismissed after this touch event. + * @return Whether the magnifier should be shown at the computed coordinates or dismissed. + */ + private boolean obtainMagnifierShowCoordinates(@NonNull final MotionEvent event, + final PointF showPosInView) { final int trigger = getMagnifierHandleTrigger(); final int offset; + final int otherHandleOffset; switch (trigger) { - case MagnifierHandleTrigger.INSERTION: // Fall through. + case MagnifierHandleTrigger.INSERTION: + offset = mTextView.getSelectionStart(); + otherHandleOffset = -1; + break; case MagnifierHandleTrigger.SELECTION_START: offset = mTextView.getSelectionStart(); + otherHandleOffset = mTextView.getSelectionEnd(); break; case MagnifierHandleTrigger.SELECTION_END: offset = mTextView.getSelectionEnd(); + otherHandleOffset = mTextView.getSelectionStart(); break; default: offset = -1; + otherHandleOffset = -1; break; } if (offset == -1) { - dismissMagnifier(); + return false; } final Layout layout = mTextView.getLayout(); final int lineNumber = layout.getLineForOffset(offset); - // Horizontally move the magnifier smoothly. + // Compute whether the selection handles are currently on the same line, and, + // in this particular case, whether the selected text is right to left. + final boolean sameLineSelection = otherHandleOffset != -1 + && lineNumber == layout.getLineForOffset(otherHandleOffset); + final boolean rtl = sameLineSelection + && (offset < otherHandleOffset) + != (getHorizontal(mTextView.getLayout(), offset) + < getHorizontal(mTextView.getLayout(), otherHandleOffset)); + + // Horizontally move the magnifier smoothly, clamp inside the current line / selection. final int[] textViewLocationOnScreen = new int[2]; mTextView.getLocationOnScreen(textViewLocationOnScreen); - final float xPosInView = event.getRawX() - textViewLocationOnScreen[0]; + final float touchXInView = event.getRawX() - textViewLocationOnScreen[0]; + float leftBound = mTextView.getTotalPaddingLeft() - mTextView.getScrollX(); + float rightBound = mTextView.getTotalPaddingLeft() - mTextView.getScrollX(); + if (sameLineSelection && ((trigger == MagnifierHandleTrigger.SELECTION_END) ^ rtl)) { + leftBound += getHorizontal(mTextView.getLayout(), otherHandleOffset); + } else { + leftBound += mTextView.getLayout().getLineLeft(lineNumber); + } + if (sameLineSelection && ((trigger == MagnifierHandleTrigger.SELECTION_START) ^ rtl)) { + rightBound += getHorizontal(mTextView.getLayout(), otherHandleOffset); + } else { + rightBound += mTextView.getLayout().getLineRight(lineNumber); + } + final float contentWidth = Math.round(mMagnifierAnimator.mMagnifier.getWidth() + / mMagnifierAnimator.mMagnifier.getZoom()); + if (touchXInView < leftBound - contentWidth / 2 + || touchXInView > rightBound + contentWidth / 2) { + // The touch is too far from the current line / selection, so hide the magnifier. + return false; + } + showPosInView.x = Math.max(leftBound, Math.min(rightBound, touchXInView)); + // Vertically snap to middle of current line. - final float yPosInView = (mTextView.getLayout().getLineTop(lineNumber) + showPosInView.y = (mTextView.getLayout().getLineTop(lineNumber) + mTextView.getLayout().getLineBottom(lineNumber)) / 2.0f + mTextView.getTotalPaddingTop() - mTextView.getScrollY(); - suspendBlink(); - mMagnifier.show(xPosInView, yPosInView); + return true; + } + + private boolean handleOverlapsMagnifier() { + final int handleY = mContainer.getDecorViewLayoutParams().y; + final int magnifierBottomWhenAtWindowTop = + mTextView.getRootWindowInsets().getSystemWindowInsetTop() + + mMagnifierAnimator.mMagnifier.getHeight(); + return handleY <= magnifierBottomWhenAtWindowTop; + } + + protected final void updateMagnifier(@NonNull final MotionEvent event) { + if (mMagnifierAnimator == null) { + return; + } + + final PointF showPosInView = new PointF(); + final boolean shouldShow = !tooLargeTextForMagnifier() + && obtainMagnifierShowCoordinates(event, showPosInView); + if (shouldShow) { + // Make the cursor visible and stop blinking. + mRenderCursorRegardlessTiming = true; + mTextView.invalidateCursorPath(); + suspendBlink(); + // Hide handle if it overlaps the magnifier. + if (handleOverlapsMagnifier()) { + setVisible(false); + } else { + setVisible(true); + } + + mMagnifierAnimator.show(showPosInView.x, showPosInView.y); + } else { + dismissMagnifier(); + } } protected final void dismissMagnifier() { - if (mMagnifier != null) { - mMagnifier.dismiss(); + if (mMagnifierAnimator != null) { + mMagnifierAnimator.dismiss(); + mRenderCursorRegardlessTiming = false; resumeBlink(); + setVisible(true); } } @@ -4853,11 +5058,11 @@ public class Editor { case MotionEvent.ACTION_DOWN: mDownPositionX = ev.getRawX(); mDownPositionY = ev.getRawY(); - showMagnifier(ev); + updateMagnifier(ev); break; case MotionEvent.ACTION_MOVE: - showMagnifier(ev); + updateMagnifier(ev); break; case MotionEvent.ACTION_UP: @@ -5211,11 +5416,11 @@ public class Editor { // re-engages the handle. mTouchWordDelta = 0.0f; mPrevX = UNSET_X_VALUE; - showMagnifier(event); + updateMagnifier(event); break; case MotionEvent.ACTION_MOVE: - showMagnifier(event); + updateMagnifier(event); break; case MotionEvent.ACTION_UP: diff --git a/android/widget/GridLayout.java b/android/widget/GridLayout.java index 012b918f..3aae8497 100644 --- a/android/widget/GridLayout.java +++ b/android/widget/GridLayout.java @@ -904,6 +904,9 @@ public class GridLayout extends ViewGroup { } } + /** + * @hide + */ @Override protected void onDebugDrawMargins(Canvas canvas, Paint paint) { // Apply defaults, so as to remove UNDEFINED values @@ -919,6 +922,9 @@ public class GridLayout extends ViewGroup { } } + /** + * @hide + */ @Override protected void onDebugDraw(Canvas canvas) { Paint paint = new Paint(); diff --git a/android/widget/ImageView.java b/android/widget/ImageView.java index 1dc5b44b..4b951fa1 100644 --- a/android/widget/ImageView.java +++ b/android/widget/ImageView.java @@ -23,10 +23,12 @@ import android.annotation.TestApi; import android.content.ContentResolver; import android.content.Context; import android.content.res.ColorStateList; +import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.ColorFilter; +import android.graphics.ImageDecoder; import android.graphics.Matrix; import android.graphics.PixelFormat; import android.graphics.PorterDuff; @@ -53,7 +55,6 @@ import android.widget.RemoteViews.RemoteView; import com.android.internal.R; import java.io.IOException; -import java.io.InputStream; /** * Displays image resources, for example {@link android.graphics.Bitmap} @@ -946,21 +947,15 @@ public class ImageView extends View { } } else if (ContentResolver.SCHEME_CONTENT.equals(scheme) || ContentResolver.SCHEME_FILE.equals(scheme)) { - InputStream stream = null; try { - stream = mContext.getContentResolver().openInputStream(uri); - return Drawable.createFromResourceStream(sCompatUseCorrectStreamDensity - ? getResources() : null, null, stream, null); - } catch (Exception e) { + Resources res = sCompatUseCorrectStreamDensity ? getResources() : null; + ImageDecoder.Source src = ImageDecoder.createSource(mContext.getContentResolver(), + uri, res); + return ImageDecoder.decodeDrawable(src, (decoder, info, s) -> { + decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE); + }); + } catch (IOException e) { Log.w(LOG_TAG, "Unable to open content: " + uri, e); - } finally { - if (stream != null) { - try { - stream.close(); - } catch (IOException e) { - Log.w(LOG_TAG, "Unable to close content: " + uri, e); - } - } } } else { return Drawable.createFromPath(uri.toString()); diff --git a/android/widget/LinearLayout.java b/android/widget/LinearLayout.java index 7ea1f1ed..d32e93c7 100644 --- a/android/widget/LinearLayout.java +++ b/android/widget/LinearLayout.java @@ -917,7 +917,7 @@ public class LinearLayout extends ViewGroup { // measurement on any children, we need to measure them now. int remainingExcess = heightSize - mTotalLength + (mAllowInconsistentMeasurement ? 0 : consumedExcessSpace); - if (skippedMeasure || remainingExcess != 0 && totalWeight > 0.0f) { + if (skippedMeasure || totalWeight > 0.0f) { float remainingWeightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight; mTotalLength = 0; @@ -1300,7 +1300,7 @@ public class LinearLayout extends ViewGroup { // measurement on any children, we need to measure them now. int remainingExcess = widthSize - mTotalLength + (mAllowInconsistentMeasurement ? 0 : usedExcessSpace); - if (skippedMeasure || remainingExcess != 0 && totalWeight > 0.0f) { + if (skippedMeasure || totalWeight > 0.0f) { float remainingWeightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight; maxAscent[0] = maxAscent[1] = maxAscent[2] = maxAscent[3] = -1; diff --git a/android/widget/Magnifier.java b/android/widget/Magnifier.java index 310b1708..5eb66999 100644 --- a/android/widget/Magnifier.java +++ b/android/widget/Magnifier.java @@ -19,21 +19,38 @@ package android.widget; import android.annotation.FloatRange; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.TestApi; import android.annotation.UiThread; import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.Outline; +import android.graphics.Paint; +import android.graphics.PixelFormat; import android.graphics.Point; import android.graphics.PointF; import android.graphics.Rect; import android.os.Handler; -import android.view.Gravity; +import android.os.HandlerThread; +import android.os.Message; +import android.view.ContextThemeWrapper; +import android.view.Display; +import android.view.DisplayListCanvas; import android.view.LayoutInflater; import android.view.PixelCopy; +import android.view.RenderNode; import android.view.Surface; +import android.view.SurfaceControl; +import android.view.SurfaceHolder; +import android.view.SurfaceSession; import android.view.SurfaceView; +import android.view.ThreadedRenderer; import android.view.View; -import android.view.ViewParent; +import android.view.ViewRootImpl; +import com.android.internal.R; import com.android.internal.util.Preconditions; /** @@ -43,33 +60,45 @@ import com.android.internal.util.Preconditions; public final class Magnifier { // Use this to specify that a previous configuration value does not exist. private static final int NONEXISTENT_PREVIOUS_CONFIG_VALUE = -1; + // The callbacks of the pixel copy requests will be invoked on + // the Handler of this Thread when the copy is finished. + private static final HandlerThread sPixelCopyHandlerThread = + new HandlerThread("magnifier pixel copy result handler"); + // The view to which this magnifier is attached. private final View mView; // The coordinates of the view in the surface. private final int[] mViewCoordinatesInSurface; // The window containing the magnifier. - private final PopupWindow mWindow; + private InternalPopupWindow mWindow; // The center coordinates of the window containing the magnifier. private final Point mWindowCoords = new Point(); // The width of the window containing the magnifier. private final int mWindowWidth; // The height of the window containing the magnifier. private final int mWindowHeight; - // The bitmap used to display the contents of the magnifier. - private final Bitmap mBitmap; + // The zoom applied to the view region copied to the magnifier window. + private final float mZoom; + // The width of the bitmaps where the magnifier content is copied. + private final int mBitmapWidth; + // The height of the bitmaps where the magnifier content is copied. + private final int mBitmapHeight; + // The elevation of the window containing the magnifier. + private final float mWindowElevation; + // The corner radius of the window containing the magnifier. + private final float mWindowCornerRadius; // The center coordinates of the content that is to be magnified. private final Point mCenterZoomCoords = new Point(); - // The callback of the pixel copy request will be invoked on this Handler when - // the copy is finished. - private final Handler mPixelCopyHandler = Handler.getMain(); - // Current magnification scale. - private final float mZoomScale; // Variables holding previous states, used for detecting redundant calls and invalidation. private final Point mPrevStartCoordsInSurface = new Point( NONEXISTENT_PREVIOUS_CONFIG_VALUE, NONEXISTENT_PREVIOUS_CONFIG_VALUE); private final PointF mPrevPosInView = new PointF( NONEXISTENT_PREVIOUS_CONFIG_VALUE, NONEXISTENT_PREVIOUS_CONFIG_VALUE); + // Rectangle defining the view surface area we pixel copy content from. private final Rect mPixelCopyRequestRect = new Rect(); + // Lock to synchronize between the UI thread and the thread that handles pixel copy results. + // Only sync mWindow writes from UI thread with mWindow reads from sPixelCopyHandlerThread. + private final Object mLock = new Object(); /** * Initializes a magnifier. @@ -79,32 +108,36 @@ public final class Magnifier { public Magnifier(@NonNull View view) { mView = Preconditions.checkNotNull(view); final Context context = mView.getContext(); - final float elevation = context.getResources().getDimension( - com.android.internal.R.dimen.magnifier_elevation); - final View content = LayoutInflater.from(context).inflate( - com.android.internal.R.layout.magnifier, null); - content.findViewById(com.android.internal.R.id.magnifier_inner).setClipToOutline(true); - mWindowWidth = context.getResources().getDimensionPixelSize( - com.android.internal.R.dimen.magnifier_width); - mWindowHeight = context.getResources().getDimensionPixelSize( - com.android.internal.R.dimen.magnifier_height); - mZoomScale = context.getResources().getFloat( - com.android.internal.R.dimen.magnifier_zoom_scale); + final View content = LayoutInflater.from(context).inflate(R.layout.magnifier, null); + content.findViewById(R.id.magnifier_inner).setClipToOutline(true); + mWindowWidth = context.getResources().getDimensionPixelSize(R.dimen.magnifier_width); + mWindowHeight = context.getResources().getDimensionPixelSize(R.dimen.magnifier_height); + mWindowElevation = context.getResources().getDimension(R.dimen.magnifier_elevation); + mWindowCornerRadius = getDeviceDefaultDialogCornerRadius(); + mZoom = context.getResources().getFloat(R.dimen.magnifier_zoom_scale); + mBitmapWidth = Math.round(mWindowWidth / mZoom); + mBitmapHeight = Math.round(mWindowHeight / mZoom); // The view's surface coordinates will not be updated until the magnifier is first shown. mViewCoordinatesInSurface = new int[2]; + } - mWindow = new PopupWindow(context); - mWindow.setContentView(content); - mWindow.setWidth(mWindowWidth); - mWindow.setHeight(mWindowHeight); - mWindow.setElevation(elevation); - mWindow.setTouchable(false); - mWindow.setBackgroundDrawable(null); + static { + sPixelCopyHandlerThread.start(); + } - final int bitmapWidth = Math.round(mWindowWidth / mZoomScale); - final int bitmapHeight = Math.round(mWindowHeight / mZoomScale); - mBitmap = Bitmap.createBitmap(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888); - getImageView().setImageBitmap(mBitmap); + /** + * Returns the device default theme dialog corner radius attribute. + * We retrieve this from the device default theme to avoid + * using the values set in the custom application themes. + */ + private float getDeviceDefaultDialogCornerRadius() { + final Context deviceDefaultContext = + new ContextThemeWrapper(mView.getContext(), R.style.Theme_DeviceDefault); + final TypedArray ta = deviceDefaultContext.obtainStyledAttributes( + new int[]{android.R.attr.dialogCornerRadius}); + final float dialogCornerRadius = ta.getDimension(0, 0); + ta.recycle(); + return dialogCornerRadius; } /** @@ -124,49 +157,28 @@ public final class Magnifier { configureCoordinates(xPosInView, yPosInView); - // Clamp startX value to avoid distorting the rendering of the magnifier content. - // For this, we compute: - // - zeroScrollXInSurface: this is the start x of mView, where this is not masked by a - // potential scrolling container. For example, if mView is a - // TextView contained in a HorizontalScrollView, - // mViewCoordinatesInSurface will reflect the surface position of - // the first text character, rather than the position of the first - // visible one. Therefore, we need to add back the amount of - // scrolling from the parent containers. - // - actualWidth: similarly, the width of a View will be larger than its actually visible - // width when it is contained in a scrolling container. We need to use - // the minimum width of a scrolling container which contains this view. - int zeroScrollXInSurface = mViewCoordinatesInSurface[0]; - int actualWidth = mView.getWidth(); - ViewParent viewParent = mView.getParent(); - while (viewParent instanceof View) { - final View container = (View) viewParent; - if (container.canScrollHorizontally(-1 /* left scroll */) - || container.canScrollHorizontally(1 /* right scroll */)) { - zeroScrollXInSurface += container.getScrollX(); - actualWidth = Math.min(actualWidth, container.getWidth() - - container.getPaddingLeft() - container.getPaddingRight()); - } - viewParent = viewParent.getParent(); - } - - final int startX = Math.max(zeroScrollXInSurface, Math.min( - mCenterZoomCoords.x - mBitmap.getWidth() / 2, - zeroScrollXInSurface + actualWidth - mBitmap.getWidth())); - final int startY = mCenterZoomCoords.y - mBitmap.getHeight() / 2; + // Clamp the startX location to avoid magnifying content which does not belong + // to the magnified view. This will not take into account overlapping views. + final Rect viewVisibleRegion = new Rect(); + mView.getGlobalVisibleRect(viewVisibleRegion); + final int startX = Math.max(viewVisibleRegion.left, Math.min( + mCenterZoomCoords.x - mBitmapWidth / 2, + viewVisibleRegion.right - mBitmapWidth)); + final int startY = mCenterZoomCoords.y - mBitmapHeight / 2; if (xPosInView != mPrevPosInView.x || yPosInView != mPrevPosInView.y) { - performPixelCopy(startX, startY); - + if (mWindow == null) { + synchronized (mLock) { + mWindow = new InternalPopupWindow(mView.getContext(), mView.getDisplay(), + getValidViewSurface(), + mWindowWidth, mWindowHeight, mWindowElevation, mWindowCornerRadius, + Handler.getMain() /* draw the magnifier on the UI thread */, mLock, + mCallback); + } + } + performPixelCopy(startX, startY, true /* update window position */); mPrevPosInView.x = xPosInView; mPrevPosInView.y = yPosInView; - - if (mWindow.isShowing()) { - mWindow.update(mWindowCoords.x, mWindowCoords.y, mWindow.getWidth(), - mWindow.getHeight()); - } else { - mWindow.showAtLocation(mView, Gravity.NO_GRAVITY, mWindowCoords.x, mWindowCoords.y); - } } } @@ -174,26 +186,73 @@ public final class Magnifier { * Dismisses the magnifier from the screen. Calling this on a dismissed magnifier is a no-op. */ public void dismiss() { - mWindow.dismiss(); + if (mWindow != null) { + synchronized (mLock) { + mWindow.destroy(); + mWindow = null; + } + mPrevPosInView.x = NONEXISTENT_PREVIOUS_CONFIG_VALUE; + mPrevPosInView.y = NONEXISTENT_PREVIOUS_CONFIG_VALUE; + mPrevStartCoordsInSurface.x = NONEXISTENT_PREVIOUS_CONFIG_VALUE; + mPrevStartCoordsInSurface.y = NONEXISTENT_PREVIOUS_CONFIG_VALUE; + } } /** * Forces the magnifier to update its content. It uses the previous coordinates passed to * {@link #show(float, float)}. This only happens if the magnifier is currently showing. - * - * @hide */ public void update() { - if (mWindow.isShowing()) { - // Update the contents shown in the magnifier. - performPixelCopy(mPrevStartCoordsInSurface.x, mPrevStartCoordsInSurface.y); + if (mWindow != null) { + // Update the content shown in the magnifier. + performPixelCopy(mPrevStartCoordsInSurface.x, mPrevStartCoordsInSurface.y, + false /* update window position */); + } + } + + /** + * @return The width of the magnifier window, in pixels. + */ + public int getWidth() { + return mWindowWidth; + } + + /** + * @return The height of the magnifier window, in pixels. + */ + public int getHeight() { + return mWindowHeight; + } + + /** + * @return The zoom applied to the magnified view region copied to the magnifier window. + * If the zoom is x and the magnifier window size is (width, height), the original size + * of the content copied in the magnifier will be (width / x, height / x). + */ + public float getZoom() { + return mZoom; + } + + @Nullable + private Surface getValidViewSurface() { + // TODO: deduplicate this against the first part of #performPixelCopy + final Surface surface; + if (mView instanceof SurfaceView) { + surface = ((SurfaceView) mView).getHolder().getSurface(); + } else if (mView.getViewRootImpl() != null) { + surface = mView.getViewRootImpl().mSurface; + } else { + surface = null; } + + return (surface != null && surface.isValid()) ? surface : null; } - private void configureCoordinates(float xPosInView, float yPosInView) { + private void configureCoordinates(final float xPosInView, final float yPosInView) { + // Compute the coordinates of the center of the content going to be displayed in the + // magnifier. These are relative to the surface the content is copied from. final float posX; final float posY; - if (mView instanceof SurfaceView) { // No offset required if the backing Surface matches the size of the SurfaceView. posX = xPosInView; @@ -203,48 +262,429 @@ public final class Magnifier { posX = xPosInView + mViewCoordinatesInSurface[0]; posY = yPosInView + mViewCoordinatesInSurface[1]; } - mCenterZoomCoords.x = Math.round(posX); mCenterZoomCoords.y = Math.round(posY); - final int verticalMagnifierOffset = mView.getContext().getResources().getDimensionPixelSize( - com.android.internal.R.dimen.magnifier_offset); + // Compute the position of the magnifier window. Again, this has to be relative to the + // surface of the magnified view, as this surface is the parent of the magnifier surface. + final int verticalOffset = mView.getContext().getResources().getDimensionPixelSize( + R.dimen.magnifier_offset); mWindowCoords.x = mCenterZoomCoords.x - mWindowWidth / 2; - mWindowCoords.y = mCenterZoomCoords.y - mWindowHeight / 2 - verticalMagnifierOffset; - } - - private void performPixelCopy(final int startXInSurface, final int startYInSurface) { - final Surface surface = getValidViewSurface(); - if (surface != null) { - mPixelCopyRequestRect.set(startXInSurface, startYInSurface, - startXInSurface + mBitmap.getWidth(), startYInSurface + mBitmap.getHeight()); - - PixelCopy.request(surface, mPixelCopyRequestRect, mBitmap, - result -> { - getImageView().invalidate(); - mPrevStartCoordsInSurface.x = startXInSurface; - mPrevStartCoordsInSurface.y = startYInSurface; - }, - mPixelCopyHandler); - } + mWindowCoords.y = mCenterZoomCoords.y - mWindowHeight / 2 - verticalOffset; } - @Nullable - private Surface getValidViewSurface() { + private void performPixelCopy(final int startXInSurface, final int startYInSurface, + final boolean updateWindowPosition) { + // Get the view surface where the content will be copied from. final Surface surface; + final int surfaceWidth; + final int surfaceHeight; if (mView instanceof SurfaceView) { - surface = ((SurfaceView) mView).getHolder().getSurface(); + final SurfaceHolder surfaceHolder = ((SurfaceView) mView).getHolder(); + surface = surfaceHolder.getSurface(); + surfaceWidth = surfaceHolder.getSurfaceFrame().right; + surfaceHeight = surfaceHolder.getSurfaceFrame().bottom; } else if (mView.getViewRootImpl() != null) { - surface = mView.getViewRootImpl().mSurface; + final ViewRootImpl viewRootImpl = mView.getViewRootImpl(); + surface = viewRootImpl.mSurface; + surfaceWidth = viewRootImpl.getWidth(); + surfaceHeight = viewRootImpl.getHeight(); } else { surface = null; + surfaceWidth = NONEXISTENT_PREVIOUS_CONFIG_VALUE; + surfaceHeight = NONEXISTENT_PREVIOUS_CONFIG_VALUE; } - return (surface != null && surface.isValid()) ? surface : null; + if (surface == null || !surface.isValid()) { + return; + } + + // Clamp copy coordinates inside the surface to avoid displaying distorted content. + final int clampedStartXInSurface = Math.max(0, + Math.min(startXInSurface, surfaceWidth - mBitmapWidth)); + final int clampedStartYInSurface = Math.max(0, + Math.min(startYInSurface, surfaceHeight - mBitmapHeight)); + + // Clamp window coordinates inside the parent surface, to avoid displaying + // the magnifier out of screen or overlapping with system insets. + final Rect insets = mView.getRootWindowInsets().getSystemWindowInsets(); + final int windowCoordsX = Math.max(insets.left, + Math.min(surfaceWidth - mWindowWidth - insets.right, mWindowCoords.x)); + final int windowCoordsY = Math.max(insets.top, + Math.min(surfaceHeight - mWindowHeight - insets.bottom, mWindowCoords.y)); + + // Perform the pixel copy. + mPixelCopyRequestRect.set(clampedStartXInSurface, + clampedStartYInSurface, + clampedStartXInSurface + mBitmapWidth, + clampedStartYInSurface + mBitmapHeight); + final InternalPopupWindow currentWindowInstance = mWindow; + final Bitmap bitmap = + Bitmap.createBitmap(mBitmapWidth, mBitmapHeight, Bitmap.Config.ARGB_8888); + PixelCopy.request(surface, mPixelCopyRequestRect, bitmap, + result -> { + synchronized (mLock) { + if (mWindow != currentWindowInstance) { + // The magnifier was dismissed (and maybe shown again) in the meantime. + return; + } + if (updateWindowPosition) { + // TODO: pull the position update outside #performPixelCopy + mWindow.setContentPositionForNextDraw(windowCoordsX, windowCoordsY); + } + mWindow.updateContent(bitmap); + } + }, + sPixelCopyHandlerThread.getThreadHandler()); + mPrevStartCoordsInSurface.x = startXInSurface; + mPrevStartCoordsInSurface.y = startYInSurface; + } + + /** + * Magnifier's own implementation of PopupWindow-similar floating window. + * This exists to ensure frame-synchronization between window position updates and window + * content updates. By using a PopupWindow, these events would happen in different frames, + * producing a shakiness effect for the magnifier content. + */ + private static class InternalPopupWindow { + // The alpha set on the magnifier's content, which defines how + // prominent the white background is. + private static final int CONTENT_BITMAP_ALPHA = 242; + + // Display associated to the view the magnifier is attached to. + private final Display mDisplay; + // The size of the content of the magnifier. + private final int mContentWidth; + private final int mContentHeight; + // The size of the allocated surface. + private final int mSurfaceWidth; + private final int mSurfaceHeight; + // The insets of the content inside the allocated surface. + private final int mOffsetX; + private final int mOffsetY; + // The surface we allocate for the magnifier content + shadow. + private final SurfaceSession mSurfaceSession; + private final SurfaceControl mSurfaceControl; + private final Surface mSurface; + // The renderer used for the allocated surface. + private final ThreadedRenderer.SimpleRenderer mRenderer; + // The RenderNode used to draw the magnifier content in the surface. + private final RenderNode mBitmapRenderNode; + // The job that will be post'd to apply the pending magnifier updates to the surface. + private final Runnable mMagnifierUpdater; + // The handler where the magnifier updater jobs will be post'd. + private final Handler mHandler; + // The callback to be run after the next draw. Only used for testing. + private Callback mCallback; + + // Members below describe the state of the magnifier. Reads/writes to them + // have to be synchronized between the UI thread and the thread that handles + // the pixel copy results. This is the purpose of mLock. + private final Object mLock; + // Whether a magnifier frame draw is currently pending in the UI thread queue. + private boolean mFrameDrawScheduled; + // The content bitmap. + private Bitmap mBitmap; + // Whether the next draw will be the first one for the current instance. + private boolean mFirstDraw = true; + // The window position in the parent surface. Might be applied during the next draw, + // when mPendingWindowPositionUpdate is true. + private int mWindowPositionX; + private int mWindowPositionY; + private boolean mPendingWindowPositionUpdate; + + // The lock used to synchronize the UI and render threads when a #destroy + // is performed on the UI thread and a frame callback on the render thread. + // When both mLock and mDestroyLock need to be held at the same time, + // mDestroyLock should be acquired before mLock in order to avoid deadlocks. + private final Object mDestroyLock = new Object(); + + InternalPopupWindow(final Context context, final Display display, + final Surface parentSurface, + final int width, final int height, final float elevation, final float cornerRadius, + final Handler handler, final Object lock, final Callback callback) { + mDisplay = display; + mLock = lock; + mCallback = callback; + + mContentWidth = width; + mContentHeight = height; + mOffsetX = (int) (0.1f * width); + mOffsetY = (int) (0.1f * height); + // Setup the surface we will use for drawing the content and shadow. + mSurfaceWidth = mContentWidth + 2 * mOffsetX; + mSurfaceHeight = mContentHeight + 2 * mOffsetY; + mSurfaceSession = new SurfaceSession(parentSurface); + mSurfaceControl = new SurfaceControl.Builder(mSurfaceSession) + .setFormat(PixelFormat.TRANSLUCENT) + .setSize(mSurfaceWidth, mSurfaceHeight) + .setName("magnifier surface") + .setFlags(SurfaceControl.HIDDEN) + .build(); + mSurface = new Surface(); + mSurface.copyFrom(mSurfaceControl); + + // Setup the RenderNode tree. The root has only one child, which contains the bitmap. + mRenderer = new ThreadedRenderer.SimpleRenderer( + context, + "magnifier renderer", + mSurface + ); + mBitmapRenderNode = createRenderNodeForBitmap( + "magnifier content", + elevation, + cornerRadius + ); + + final DisplayListCanvas canvas = mRenderer.getRootNode().start(width, height); + try { + canvas.insertReorderBarrier(); + canvas.drawRenderNode(mBitmapRenderNode); + canvas.insertInorderBarrier(); + } finally { + mRenderer.getRootNode().end(canvas); + } + + // Initialize the update job and the handler where this will be post'd. + mHandler = handler; + mMagnifierUpdater = this::doDraw; + mFrameDrawScheduled = false; + } + + private RenderNode createRenderNodeForBitmap(final String name, + final float elevation, final float cornerRadius) { + final RenderNode bitmapRenderNode = RenderNode.create(name, null); + + // Define the position of the bitmap in the parent render node. The surface regions + // outside the bitmap are used to draw elevation. + bitmapRenderNode.setLeftTopRightBottom(mOffsetX, mOffsetY, + mOffsetX + mContentWidth, mOffsetY + mContentHeight); + bitmapRenderNode.setElevation(elevation); + + final Outline outline = new Outline(); + outline.setRoundRect(0, 0, mContentWidth, mContentHeight, cornerRadius); + outline.setAlpha(1.0f); + bitmapRenderNode.setOutline(outline); + bitmapRenderNode.setClipToOutline(true); + + // Create a dummy draw, which will be replaced later with real drawing. + final DisplayListCanvas canvas = bitmapRenderNode.start(mContentWidth, mContentHeight); + try { + canvas.drawColor(0xFF00FF00); + } finally { + bitmapRenderNode.end(canvas); + } + + return bitmapRenderNode; + } + + /** + * Sets the position of the magnifier content relative to the parent surface. + * The position update will happen in the same frame with the next draw. + * The method has to be called in a context that holds {@link #mLock}. + * + * @param contentX the x coordinate of the content + * @param contentY the y coordinate of the content + */ + public void setContentPositionForNextDraw(final int contentX, final int contentY) { + mWindowPositionX = contentX - mOffsetX; + mWindowPositionY = contentY - mOffsetY; + mPendingWindowPositionUpdate = true; + requestUpdate(); + } + + /** + * Sets the content that should be displayed in the magnifier. + * The update happens immediately, and possibly triggers a pending window movement set + * by {@link #setContentPositionForNextDraw(int, int)}. + * The method has to be called in a context that holds {@link #mLock}. + * + * @param bitmap the content bitmap + */ + public void updateContent(final @NonNull Bitmap bitmap) { + if (mBitmap != null) { + mBitmap.recycle(); + } + mBitmap = bitmap; + requestUpdate(); + } + + private void requestUpdate() { + if (mFrameDrawScheduled) { + return; + } + final Message request = Message.obtain(mHandler, mMagnifierUpdater); + request.setAsynchronous(true); + request.sendToTarget(); + mFrameDrawScheduled = true; + } + + /** + * Destroys this instance. + */ + public void destroy() { + synchronized (mDestroyLock) { + mSurface.destroy(); + } + synchronized (mLock) { + mRenderer.destroy(); + mSurfaceControl.destroy(); + mSurfaceSession.kill(); + mBitmapRenderNode.destroy(); + mHandler.removeCallbacks(mMagnifierUpdater); + if (mBitmap != null) { + mBitmap.recycle(); + } + } + } + + private void doDraw() { + final ThreadedRenderer.FrameDrawingCallback callback; + + // Draw the current bitmap to the surface, and prepare the callback which updates the + // surface position. These have to be in the same synchronized block, in order to + // guarantee the consistency between the bitmap content and the surface position. + synchronized (mLock) { + if (!mSurface.isValid()) { + // Probably #destroy() was called for the current instance, so we skip the draw. + return; + } + + final DisplayListCanvas canvas = + mBitmapRenderNode.start(mContentWidth, mContentHeight); + try { + canvas.drawColor(Color.WHITE); + + final Rect srcRect = new Rect(0, 0, mBitmap.getWidth(), mBitmap.getHeight()); + final Rect dstRect = new Rect(0, 0, mContentWidth, mContentHeight); + final Paint paint = new Paint(); + paint.setFilterBitmap(true); + paint.setAlpha(CONTENT_BITMAP_ALPHA); + canvas.drawBitmap(mBitmap, srcRect, dstRect, paint); + } finally { + mBitmapRenderNode.end(canvas); + } + + if (mPendingWindowPositionUpdate || mFirstDraw) { + // If the window has to be shown or moved, defer this until the next draw. + final boolean firstDraw = mFirstDraw; + mFirstDraw = false; + final boolean updateWindowPosition = mPendingWindowPositionUpdate; + mPendingWindowPositionUpdate = false; + final int pendingX = mWindowPositionX; + final int pendingY = mWindowPositionY; + + callback = frame -> { + synchronized (mDestroyLock) { + if (!mSurface.isValid()) { + return; + } + synchronized (mLock) { + mRenderer.setLightCenter(mDisplay, pendingX, pendingY); + // Show or move the window at the content draw frame. + SurfaceControl.openTransaction(); + mSurfaceControl.deferTransactionUntil(mSurface, frame); + if (updateWindowPosition) { + mSurfaceControl.setPosition(pendingX, pendingY); + } + if (firstDraw) { + mSurfaceControl.show(); + } + SurfaceControl.closeTransaction(); + } + } + }; + } else { + callback = null; + } + + mFrameDrawScheduled = false; + } + + mRenderer.draw(callback); + if (mCallback != null) { + mCallback.onOperationComplete(); + } + } + } + + // The rest of the file consists of test APIs. + + /** + * See {@link #setOnOperationCompleteCallback(Callback)}. + */ + @TestApi + private Callback mCallback; + + /** + * Sets a callback which will be invoked at the end of the next + * {@link #show(float, float)} or {@link #update()} operation. + * + * @hide + */ + @TestApi + public void setOnOperationCompleteCallback(final Callback callback) { + mCallback = callback; + if (mWindow != null) { + mWindow.mCallback = callback; + } + } + + /** + * @return the content being currently displayed in the magnifier, as bitmap + * + * @hide + */ + @TestApi + public @Nullable Bitmap getContent() { + if (mWindow == null) { + return null; + } + synchronized (mWindow.mLock) { + return Bitmap.createScaledBitmap(mWindow.mBitmap, mWindowWidth, mWindowHeight, true); + } } - private ImageView getImageView() { - return mWindow.getContentView().findViewById( - com.android.internal.R.id.magnifier_image); + /** + * @return the position of the magnifier window relative to the screen + * + * @hide + */ + @TestApi + public Rect getWindowPositionOnScreen() { + final int[] viewLocationOnScreen = new int[2]; + mView.getLocationOnScreen(viewLocationOnScreen); + final int[] viewLocationInSurface = new int[2]; + mView.getLocationInSurface(viewLocationInSurface); + + final int left = mWindowCoords.x + viewLocationOnScreen[0] - viewLocationInSurface[0]; + final int top = mWindowCoords.y + viewLocationOnScreen[1] - viewLocationInSurface[1]; + return new Rect(left, top, left + mWindowWidth, top + mWindowHeight); + } + + /** + * @return the size of the magnifier window in dp + * + * @hide + */ + @TestApi + public static PointF getMagnifierDefaultSize() { + final Resources resources = Resources.getSystem(); + final float density = resources.getDisplayMetrics().density; + final PointF size = new PointF(); + size.x = resources.getDimension(R.dimen.magnifier_width) / density; + size.y = resources.getDimension(R.dimen.magnifier_height) / density; + return size; + } + + /** + * @hide + */ + @TestApi + public interface Callback { + /** + * Callback called after the drawing for a magnifier update has happened. + */ + void onOperationComplete(); } } diff --git a/android/widget/MediaControlView2.java b/android/widget/MediaControlView2.java index f1d633a2..f52854a8 100644 --- a/android/widget/MediaControlView2.java +++ b/android/widget/MediaControlView2.java @@ -20,18 +20,20 @@ import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; +import android.media.SessionToken2; import android.media.session.MediaController; import android.media.update.ApiLoader; import android.media.update.MediaControlView2Provider; -import android.media.update.ViewProvider; +import android.media.update.ViewGroupHelper; import android.util.AttributeSet; -import android.view.KeyEvent; -import android.view.MotionEvent; import android.view.View; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; + +// TODO: Use link annotation to refer VideoView2 once VideoView2 became unhidden. /** + * @hide * A View that contains the controls for MediaPlayer2. * It provides a wide range of UI including buttons such as "Play/Pause", "Rewind", "Fast Forward", * "Subtitle", "Full Screen", and it is also possible to add multiple custom buttons. @@ -42,15 +44,24 @@ import java.lang.annotation.RetentionPolicy; * adds it to the view. * 2) Initialize MediaControlView2 programmatically and add it to a ViewGroup instance. * - * In the first option, VideoView2 automatically connects MediaControlView2 to MediaController2, + * In the first option, VideoView2 automatically connects MediaControlView2 to MediaController, * which is necessary to communicate with MediaSession2. In the second option, however, the - * developer needs to manually retrieve a MediaController2 instance and set it to MediaControlView2 - * by calling setController(MediaController2 controller). + * developer needs to manually retrieve a MediaController instance and set it to MediaControlView2 + * by calling setController(MediaController controller). * - * TODO PUBLIC API - * @hide + * <p> + * There is no separate method that handles the show/hide behavior for MediaControlView2. Instead, + * one can directly change the visibility of this view by calling View.setVisibility(int). The + * values supported are View.VISIBLE and View.GONE. + * In addition, the following customization is supported: + * Set focus to the play/pause button by calling requestPlayButtonFocus(). + * + * <p> + * It is also possible to add custom buttons with custom icons and actions inside MediaControlView2. + * Those buttons will be shown when the overflow button is clicked. + * See VideoView2#setCustomActions for more details on how to add. */ -public class MediaControlView2 extends FrameLayout { +public class MediaControlView2 extends ViewGroupHelper<MediaControlView2Provider> { /** @hide */ @IntDef({ BUTTON_PLAY_PAUSE, @@ -68,20 +79,62 @@ public class MediaControlView2 extends FrameLayout { @Retention(RetentionPolicy.SOURCE) public @interface Button {} + /** + * MediaControlView2 button value for playing and pausing media. + * @hide + */ public static final int BUTTON_PLAY_PAUSE = 1; + /** + * MediaControlView2 button value for jumping 30 seconds forward. + * @hide + */ public static final int BUTTON_FFWD = 2; + /** + * MediaControlView2 button value for jumping 10 seconds backward. + * @hide + */ public static final int BUTTON_REW = 3; + /** + * MediaControlView2 button value for jumping to next media. + * @hide + */ public static final int BUTTON_NEXT = 4; + /** + * MediaControlView2 button value for jumping to previous media. + * @hide + */ public static final int BUTTON_PREV = 5; + /** + * MediaControlView2 button value for showing/hiding subtitle track. + * @hide + */ public static final int BUTTON_SUBTITLE = 6; + /** + * MediaControlView2 button value for toggling full screen. + * @hide + */ public static final int BUTTON_FULL_SCREEN = 7; + /** + * MediaControlView2 button value for showing/hiding overflow buttons. + * @hide + */ public static final int BUTTON_OVERFLOW = 8; + /** + * MediaControlView2 button value for muting audio. + * @hide + */ public static final int BUTTON_MUTE = 9; + /** + * MediaControlView2 button value for adjusting aspect ratio of view. + * @hide + */ public static final int BUTTON_ASPECT_RATIO = 10; + /** + * MediaControlView2 button value for showing/hiding settings page. + * @hide + */ public static final int BUTTON_SETTINGS = 11; - private final MediaControlView2Provider mProvider; - public MediaControlView2(@NonNull Context context) { this(context, null); } @@ -91,189 +144,86 @@ public class MediaControlView2 extends FrameLayout { } public MediaControlView2(@NonNull Context context, @Nullable AttributeSet attrs, - int defStyleAttr) { + int defStyleAttr) { this(context, attrs, defStyleAttr, 0); } public MediaControlView2(@NonNull Context context, @Nullable AttributeSet attrs, - int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - - mProvider = ApiLoader.getProvider(context) - .createMediaControlView2(this, new SuperProvider()); - } - - /** - * @hide - */ - public MediaControlView2Provider getProvider() { - return mProvider; - } - - /** - * Sets MediaController2 instance to control corresponding MediaSession2. - */ - public void setController(MediaController controller) { - mProvider.setController_impl(controller); - } - - /** - * Shows the control view on screen. It will disappear automatically after 3 seconds of - * inactivity. - */ - public void show() { - mProvider.show_impl(); - } - - /** - * Shows the control view on screen. It will disappear automatically after {@code timeout} - * milliseconds of inactivity. - */ - public void show(int timeout) { - mProvider.show_impl(timeout); - } - - /** - * Returns whether the control view is currently shown or hidden. - */ - public boolean isShowing() { - return mProvider.isShowing_impl(); + int defStyleAttr, int defStyleRes) { + super((instance, superProvider, privateProvider) -> + ApiLoader.getProvider().createMediaControlView2( + (MediaControlView2) instance, superProvider, privateProvider, + attrs, defStyleAttr, defStyleRes), + context, attrs, defStyleAttr, defStyleRes); + mProvider.initialize(attrs, defStyleAttr, defStyleRes); } /** - * Hide the control view from the screen. + * Sets MediaSession2 token to control corresponding MediaSession2. */ - public void hide() { - mProvider.hide_impl(); + public void setMediaSessionToken(SessionToken2 token) { + mProvider.setMediaSessionToken_impl(token); } /** - * If the media selected has a subtitle track, calling this method will display the subtitle at - * the bottom of the view. If a media has multiple subtitle tracks, this method will select the - * first one of them. + * Registers a callback to be invoked when the fullscreen mode should be changed. + * @param l The callback that will be run */ - public void showSubtitle() { - mProvider.showSubtitle_impl(); + public void setOnFullScreenListener(OnFullScreenListener l) { + mProvider.setOnFullScreenListener_impl(l); } /** - * Hides the currently displayed subtitle. + * @hide TODO: remove once the implementation is revised */ - public void hideSubtitle() { - mProvider.hideSubtitle_impl(); + public void setController(MediaController controller) { + mProvider.setController_impl(controller); } /** - * Set listeners for previous and next buttons to customize the behavior of clicking them. - * The UI for these buttons are provided as default and will be automatically displayed when - * this method is called. + * Changes the visibility state of an individual button. Default value is View.Visible. * - * @param next Listener for clicking next button - * @param prev Listener for clicking previous button + * @param button the {@code Button} assigned to individual buttons + * <ul> + * <li>{@link #BUTTON_PLAY_PAUSE} + * <li>{@link #BUTTON_FFWD} + * <li>{@link #BUTTON_REW} + * <li>{@link #BUTTON_NEXT} + * <li>{@link #BUTTON_PREV} + * <li>{@link #BUTTON_SUBTITLE} + * <li>{@link #BUTTON_FULL_SCREEN} + * <li>{@link #BUTTON_MUTE} + * <li>{@link #BUTTON_OVERFLOW} + * <li>{@link #BUTTON_ASPECT_RATIO} + * <li>{@link #BUTTON_SETTINGS} + * </ul> + * @param visibility One of {@link #VISIBLE}, {@link #INVISIBLE}, or {@link #GONE}. + * @hide */ - public void setPrevNextListeners(View.OnClickListener next, View.OnClickListener prev) { - mProvider.setPrevNextListeners_impl(next, prev); + public void setButtonVisibility(@Button int button, @Visibility int visibility) { + mProvider.setButtonVisibility_impl(button, visibility); } /** - * Hides the specified button from view. - * - * @param button the constant integer assigned to individual buttons - * @param visible whether the button should be visible or not + * Requests focus for the play/pause button. */ - public void setButtonVisibility(int button, boolean visible) { - mProvider.setButtonVisibility_impl(button, visible); - } - - @Override - protected void onAttachedToWindow() { - mProvider.onAttachedToWindow_impl(); - } - - @Override - protected void onDetachedFromWindow() { - mProvider.onDetachedFromWindow_impl(); - } - - @Override - public CharSequence getAccessibilityClassName() { - return mProvider.getAccessibilityClassName_impl(); + public void requestPlayButtonFocus() { + mProvider.requestPlayButtonFocus_impl(); } @Override - public boolean onTouchEvent(MotionEvent ev) { - return mProvider.onTouchEvent_impl(ev); + protected void onLayout(boolean changed, int l, int t, int r, int b) { + mProvider.onLayout_impl(changed, l, t, r, b); } - @Override - public boolean onTrackballEvent(MotionEvent ev) { - return mProvider.onTrackballEvent_impl(ev); - } - - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { - return mProvider.onKeyDown_impl(keyCode, event); - } - - @Override - public void onFinishInflate() { - mProvider.onFinishInflate_impl(); - } - - @Override - public boolean dispatchKeyEvent(KeyEvent event) { - return mProvider.dispatchKeyEvent_impl(event); - } - - @Override - public void setEnabled(boolean enabled) { - mProvider.setEnabled_impl(enabled); - } - - private class SuperProvider implements ViewProvider { - @Override - public void onAttachedToWindow_impl() { - MediaControlView2.super.onAttachedToWindow(); - } - - @Override - public void onDetachedFromWindow_impl() { - MediaControlView2.super.onDetachedFromWindow(); - } - - @Override - public CharSequence getAccessibilityClassName_impl() { - return MediaControlView2.super.getAccessibilityClassName(); - } - - @Override - public boolean onTouchEvent_impl(MotionEvent ev) { - return MediaControlView2.super.onTouchEvent(ev); - } - - @Override - public boolean onTrackballEvent_impl(MotionEvent ev) { - return MediaControlView2.super.onTrackballEvent(ev); - } - - @Override - public boolean onKeyDown_impl(int keyCode, KeyEvent event) { - return MediaControlView2.super.onKeyDown(keyCode, event); - } - - @Override - public void onFinishInflate_impl() { - MediaControlView2.super.onFinishInflate(); - } - - @Override - public boolean dispatchKeyEvent_impl(KeyEvent event) { - return MediaControlView2.super.dispatchKeyEvent(event); - } - - @Override - public void setEnabled_impl(boolean enabled) { - MediaControlView2.super.setEnabled(enabled); - } + /** + * Interface definition of a callback to be invoked to inform the fullscreen mode is changed. + * Application should handle the fullscreen mode accordingly. + */ + public interface OnFullScreenListener { + /** + * Called to indicate a fullscreen mode change. + */ + void onFullScreen(View view, boolean fullScreen); } } diff --git a/android/widget/PopupWindow.java b/android/widget/PopupWindow.java index e91db139..9553cf5e 100644 --- a/android/widget/PopupWindow.java +++ b/android/widget/PopupWindow.java @@ -1583,7 +1583,7 @@ public class PopupWindow { * * @hide */ - protected final boolean findDropDownPosition(View anchor, WindowManager.LayoutParams outParams, + protected boolean findDropDownPosition(View anchor, WindowManager.LayoutParams outParams, int xOffset, int yOffset, int width, int height, int gravity, boolean allowScroll) { final int anchorHeight = anchor.getHeight(); final int anchorWidth = anchor.getWidth(); @@ -2563,7 +2563,9 @@ public class PopupWindow { public void onViewDetachedFromWindow(View v) { v.removeOnAttachStateChangeListener(this); - TransitionManager.endTransitions(PopupDecorView.this); + if (isAttachedToWindow()) { + TransitionManager.endTransitions(PopupDecorView.this); + } } }; diff --git a/android/widget/RadioGroup.java b/android/widget/RadioGroup.java index 5c4d4d2a..c9871479 100644 --- a/android/widget/RadioGroup.java +++ b/android/widget/RadioGroup.java @@ -183,13 +183,17 @@ public class RadioGroup extends LinearLayout { } private void setCheckedId(@IdRes int id) { + boolean changed = id != mCheckedId; mCheckedId = id; + if (mOnCheckedChangeListener != null) { mOnCheckedChangeListener.onCheckedChanged(this, mCheckedId); } - final AutofillManager afm = mContext.getSystemService(AutofillManager.class); - if (afm != null) { - afm.notifyValueChanged(this); + if (changed) { + final AutofillManager afm = mContext.getSystemService(AutofillManager.class); + if (afm != null) { + afm.notifyValueChanged(this); + } } } diff --git a/android/widget/RelativeLayout.java b/android/widget/RelativeLayout.java index 75fc5386..bbdf15c8 100644 --- a/android/widget/RelativeLayout.java +++ b/android/widget/RelativeLayout.java @@ -1182,12 +1182,12 @@ public class RelativeLayout extends ViewGroup { * determine where to position the view on the screen. If the view is not contained * within a relative layout, these attributes are ignored. * - * See the <a href=“https://developer.android.com/guide/topics/ui/layout/relative.html”> + * See the <a href="/guide/topics/ui/layout/relative.html"> * Relative Layout</a> guide for example code demonstrating how to use relative layout’s * layout parameters in a layout XML. * * To learn more about layout parameters and how they differ from typical view attributes, - * see the <a href=“https://developer.android.com/guide/topics/ui/declaring-layout.html#attributes”> + * see the <a href="/guide/topics/ui/declaring-layout.html#attributes"> * Layouts guide</a>. * * diff --git a/android/widget/RemoteViews.java b/android/widget/RemoteViews.java index a2c55b09..08513aa8 100644 --- a/android/widget/RemoteViews.java +++ b/android/widget/RemoteViews.java @@ -69,8 +69,6 @@ import com.android.internal.R; import com.android.internal.util.NotificationColorUtil; import com.android.internal.util.Preconditions; -import libcore.util.Objects; - import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -82,6 +80,7 @@ import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; +import java.util.Objects; import java.util.Stack; import java.util.concurrent.Executor; @@ -291,9 +290,9 @@ public class RemoteViews implements Parcelable, Filter { return false; } MethodKey p = (MethodKey) o; - return Objects.equal(p.targetClass, targetClass) - && Objects.equal(p.paramClass, paramClass) - && Objects.equal(p.methodName, methodName); + return Objects.equals(p.targetClass, targetClass) + && Objects.equals(p.paramClass, paramClass) + && Objects.equals(p.methodName, methodName); } @Override diff --git a/android/widget/SearchView.java b/android/widget/SearchView.java index 519a7dd8..225497b7 100644 --- a/android/widget/SearchView.java +++ b/android/widget/SearchView.java @@ -1990,28 +1990,15 @@ public class SearchView extends LinearLayout implements CollapsibleActionView { @Override public boolean onKeyPreIme(int keyCode, KeyEvent event) { - if (keyCode == KeyEvent.KEYCODE_BACK) { - // special case for the back key, we do not even try to send it - // to the drop down list but instead, consume it immediately - if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) { - KeyEvent.DispatcherState state = getKeyDispatcherState(); - if (state != null) { - state.startTracking(event, this); - } - return true; - } else if (event.getAction() == KeyEvent.ACTION_UP) { - KeyEvent.DispatcherState state = getKeyDispatcherState(); - if (state != null) { - state.handleUpEvent(event); - } - if (event.isTracking() && !event.isCanceled()) { - mSearchView.clearFocus(); - setImeVisibility(false); - return true; - } - } + final boolean consume = super.onKeyPreIme(keyCode, event); + if (consume && keyCode == KeyEvent.KEYCODE_BACK + && event.getAction() == KeyEvent.ACTION_UP) { + // If AutoCompleteTextView closed its pop-up, it will return true, in which case + // we should also close the IME. Otherwise, the popup is already closed and we can + // leave the BACK event alone. + setImeVisibility(false); } - return super.onKeyPreIme(keyCode, event); + return consume; } /** diff --git a/android/widget/SelectionActionModeHelper.java b/android/widget/SelectionActionModeHelper.java index 3bfa520c..b3327a70 100644 --- a/android/widget/SelectionActionModeHelper.java +++ b/android/widget/SelectionActionModeHelper.java @@ -33,12 +33,14 @@ import android.text.Spannable; import android.text.TextUtils; import android.util.Log; import android.view.ActionMode; +import android.view.textclassifier.Logger; +import android.view.textclassifier.SelectionEvent; +import android.view.textclassifier.SelectionEvent.InvocationMethod; import android.view.textclassifier.TextClassification; +import android.view.textclassifier.TextClassificationConstants; +import android.view.textclassifier.TextClassificationManager; import android.view.textclassifier.TextClassifier; -import android.view.textclassifier.TextLinks; import android.view.textclassifier.TextSelection; -import android.view.textclassifier.logging.SmartSelectionEventTracker; -import android.view.textclassifier.logging.SmartSelectionEventTracker.SelectionEvent; import android.widget.Editor.SelectionModifierCursorController; import com.android.internal.annotations.VisibleForTesting; @@ -65,13 +67,12 @@ public final class SelectionActionModeHelper { private static final String LOG_TAG = "SelectActionModeHelper"; - private static final boolean SMART_SELECT_ANIMATION_ENABLED = true; - private final Editor mEditor; private final TextView mTextView; private final TextClassificationHelper mTextClassificationHelper; + private final TextClassificationConstants mTextClassificationSettings; - private TextClassification mTextClassification; + @Nullable private TextClassification mTextClassification; private AsyncTask mTextClassificationAsyncTask; private final SelectionTracker mSelectionTracker; @@ -83,16 +84,17 @@ public final class SelectionActionModeHelper { SelectionActionModeHelper(@NonNull Editor editor) { mEditor = Preconditions.checkNotNull(editor); mTextView = mEditor.getTextView(); + mTextClassificationSettings = TextClassificationManager.getSettings(mTextView.getContext()); mTextClassificationHelper = new TextClassificationHelper( mTextView.getContext(), - mTextView.getTextClassifier(), + mTextView::getTextClassifier, getText(mTextView), 0, 1, mTextView.getTextLocales()); mSelectionTracker = new SelectionTracker(mTextView); - if (SMART_SELECT_ANIMATION_ENABLED) { + if (mTextClassificationSettings.isSmartSelectionAnimationEnabled()) { mSmartSelectSprite = new SmartSelectSprite(mTextView.getContext(), - mTextView::invalidate); + editor.getTextView().mHighlightColor, mTextView::invalidate); } else { mSmartSelectSprite = null; } @@ -103,14 +105,13 @@ public final class SelectionActionModeHelper { */ public void startSelectionActionModeAsync(boolean adjustSelection) { // Check if the smart selection should run for editable text. - adjustSelection &= !mTextView.isTextEditable() - || mTextView.getTextClassifier().getSettings() - .isSuggestSelectionEnabledForEditableText(); + adjustSelection &= mTextClassificationSettings.isSmartSelectionEnabled(); mSelectionTracker.onOriginalSelection( getText(mTextView), mTextView.getSelectionStart(), - mTextView.getSelectionEnd()); + mTextView.getSelectionEnd(), + false /*isLink*/); cancelAsyncTask(); if (skipTextClassification()) { startSelectionActionMode(null); @@ -124,7 +125,8 @@ public final class SelectionActionModeHelper { : mTextClassificationHelper::classifyText, mSmartSelectSprite != null ? this::startSelectionActionModeWithSmartSelectAnimation - : this::startSelectionActionMode) + : this::startSelectionActionMode, + mTextClassificationHelper::getOriginalSelection) .execute(); } } @@ -132,18 +134,19 @@ public final class SelectionActionModeHelper { /** * Starts Link ActionMode. */ - public void startLinkActionModeAsync(TextLinks.TextLink textLink) { - //TODO: tracking/logging + public void startLinkActionModeAsync(int start, int end) { + mSelectionTracker.onOriginalSelection(getText(mTextView), start, end, true /*isLink*/); cancelAsyncTask(); if (skipTextClassification()) { startLinkActionMode(null); } else { - resetTextClassificationHelper(textLink.getStart(), textLink.getEnd()); + resetTextClassificationHelper(start, end); mTextClassificationAsyncTask = new TextClassificationAsyncTask( mTextView, mTextClassificationHelper.getTimeoutDuration(), mTextClassificationHelper::classifyText, - this::startLinkActionMode) + this::startLinkActionMode, + mTextClassificationHelper::getOriginalSelection) .execute(); } } @@ -158,7 +161,8 @@ public final class SelectionActionModeHelper { mTextView, mTextClassificationHelper.getTimeoutDuration(), mTextClassificationHelper::classifyText, - this::invalidateActionMode) + this::invalidateActionMode, + mTextClassificationHelper::getOriginalSelection) .execute(); } } @@ -172,7 +176,7 @@ public final class SelectionActionModeHelper { public void onSelectionDrag() { mSelectionTracker.onSelectionAction( mTextView.getSelectionStart(), mTextView.getSelectionEnd(), - SelectionEvent.ActionType.DRAG, mTextClassification); + SelectionEvent.ACTION_DRAG, mTextClassification); } public void onTextChanged(int start, int end) { @@ -199,11 +203,15 @@ public final class SelectionActionModeHelper { } public void onDraw(final Canvas canvas) { - if (mSmartSelectSprite != null) { + if (isDrawingHighlight() && mSmartSelectSprite != null) { mSmartSelectSprite.draw(canvas); } } + public boolean isDrawingHighlight() { + return mSmartSelectSprite != null && mSmartSelectSprite.isAnimationActive(); + } + private void cancelAsyncTask() { if (mTextClassificationAsyncTask != null) { mTextClassificationAsyncTask.cancel(true); @@ -214,7 +222,7 @@ public final class SelectionActionModeHelper { private boolean skipTextClassification() { // No need to make an async call for a no-op TextClassifier. - final boolean noOpTextClassifier = mTextView.getTextClassifier() == TextClassifier.NO_OP; + final boolean noOpTextClassifier = mTextView.usesNoOpTextClassifier(); // Do not call the TextClassifier if there is no selection. final boolean noSelection = mTextView.getSelectionEnd() == mTextView.getSelectionStart(); // Do not call the TextClassifier if this is a password field. @@ -235,15 +243,15 @@ public final class SelectionActionModeHelper { @Editor.TextActionMode int actionMode, @Nullable SelectionResult result) { final CharSequence text = getText(mTextView); if (result != null && text instanceof Spannable - && (mTextView.isTextSelectable() - || mTextView.isTextEditable() - || actionMode == Editor.TextActionMode.TEXT_LINK)) { + && (mTextView.isTextSelectable() || mTextView.isTextEditable())) { // Do not change the selection if TextClassifier should be dark launched. - if (!mTextView.getTextClassifier().getSettings().isDarkLaunch()) { + if (!mTextClassificationSettings.isModelDarkLaunchEnabled()) { Selection.setSelection((Spannable) text, result.mStart, result.mEnd); mTextView.invalidate(); } mTextClassification = result.mClassification; + } else if (result != null && actionMode == Editor.TextActionMode.TEXT_LINK) { + mTextClassification = result.mClassification; } else { mTextClassification = null; } @@ -440,8 +448,7 @@ public final class SelectionActionModeHelper { selectionEnd = mTextView.getSelectionEnd(); } mTextClassificationHelper.init( - mTextView.getContext(), - mTextView.getTextClassifier(), + mTextView::getTextClassifier, getText(mTextView), selectionStart, selectionEnd, mTextView.getTextLocales()); @@ -482,7 +489,8 @@ public final class SelectionActionModeHelper { /** * Called when the original selection happens, before smart selection is triggered. */ - public void onOriginalSelection(CharSequence text, int selectionStart, int selectionEnd) { + public void onOriginalSelection( + CharSequence text, int selectionStart, int selectionEnd, boolean isLink) { // If we abandoned a selection and created a new one very shortly after, we may still // have a pending request to log ABANDON, which we flush here. mDelayedLogAbandon.flush(); @@ -491,7 +499,9 @@ public final class SelectionActionModeHelper { mOriginalEnd = mSelectionEnd = selectionEnd; mAllowReset = false; maybeInvalidateLogger(); - mLogger.logSelectionStarted(text, selectionStart); + mLogger.logSelectionStarted(mTextView.getTextClassificationSession(), + text, selectionStart, + isLink ? SelectionEvent.INVOCATION_LINK : SelectionEvent.INVOCATION_MANUAL); } /** @@ -574,7 +584,7 @@ public final class SelectionActionModeHelper { mSelectionEnd = editor.getTextView().getSelectionEnd(); mLogger.logSelectionAction( textView.getSelectionStart(), textView.getSelectionEnd(), - SelectionEvent.ActionType.RESET, null /* classification */); + SelectionEvent.ACTION_RESET, null /* classification */); } return selected; } @@ -583,7 +593,7 @@ public final class SelectionActionModeHelper { public void onTextChanged(int start, int end, TextClassification classification) { if (isSelectionStarted() && start == mSelectionStart && end == mSelectionEnd) { - onSelectionAction(start, end, SelectionEvent.ActionType.OVERTYPE, classification); + onSelectionAction(start, end, SelectionEvent.ACTION_OVERTYPE, classification); } } @@ -622,8 +632,9 @@ public final class SelectionActionModeHelper { if (mIsPending) { mLogger.logSelectionAction( mSelectionStart, mSelectionEnd, - SelectionEvent.ActionType.ABANDON, null /* classification */); + SelectionEvent.ACTION_ABANDON, null /* classification */); mSelectionStart = mSelectionEnd = -1; + mTextView.getTextClassificationSession().destroy(); mIsPending = false; } } @@ -643,43 +654,62 @@ public final class SelectionActionModeHelper { * Part selection of a word e.g. "or" is counted as selecting the * entire word i.e. equivalent to "York", and each special character is counted as a word, e.g. * "," is at [2, 3). Whitespaces are ignored. + * + * NOTE that the definition of a word is defined by the TextClassifier's Logger's token + * iterator. */ private static final class SelectionMetricsLogger { private static final String LOG_TAG = "SelectionMetricsLogger"; private static final Pattern PATTERN_WHITESPACE = Pattern.compile("\\s+"); - private final SmartSelectionEventTracker mDelegate; + private final Logger mLogger; private final boolean mEditTextLogger; - private final BreakIterator mWordIterator; + private final BreakIterator mTokenIterator; + + @Nullable private TextClassifier mClassificationSession; private int mStartIndex; private String mText; SelectionMetricsLogger(TextView textView) { Preconditions.checkNotNull(textView); - final @SmartSelectionEventTracker.WidgetType int widgetType = textView.isTextEditable() - ? SmartSelectionEventTracker.WidgetType.EDITTEXT - : (textView.isTextSelectable() - ? SmartSelectionEventTracker.WidgetType.TEXTVIEW - : SmartSelectionEventTracker.WidgetType.UNSELECTABLE_TEXTVIEW); - mDelegate = new SmartSelectionEventTracker(textView.getContext(), widgetType); + mLogger = textView.getTextClassifier().getLogger( + new Logger.Config(textView.getContext(), getWidetType(textView), null)); mEditTextLogger = textView.isTextEditable(); - mWordIterator = BreakIterator.getWordInstance(textView.getTextLocale()); + mTokenIterator = mLogger.getTokenIterator(textView.getTextLocale()); + } + + @TextClassifier.WidgetType + private static String getWidetType(TextView textView) { + if (textView.isTextEditable()) { + return TextClassifier.WIDGET_TYPE_EDITTEXT; + } + if (textView.isTextSelectable()) { + return TextClassifier.WIDGET_TYPE_TEXTVIEW; + } + return TextClassifier.WIDGET_TYPE_UNSELECTABLE_TEXTVIEW; } - public void logSelectionStarted(CharSequence text, int index) { + public void logSelectionStarted( + TextClassifier classificationSession, + CharSequence text, int index, + @InvocationMethod int invocationMethod) { try { Preconditions.checkNotNull(text); Preconditions.checkArgumentInRange(index, 0, text.length(), "index"); if (mText == null || !mText.contentEquals(text)) { mText = text.toString(); } - mWordIterator.setText(mText); + mTokenIterator.setText(mText); mStartIndex = index; - mDelegate.logEvent(SelectionEvent.selectionStarted(0)); + mLogger.logSelectionStartedEvent(invocationMethod, 0); + // TODO: Remove the above legacy logging. + mClassificationSession = classificationSession; + mClassificationSession.onSelectionEvent( + SelectionEvent.createSelectionStartedEvent(invocationMethod, 0)); } catch (Exception e) { // Avoid crashes due to logging. - Log.d(LOG_TAG, e.getMessage()); + Log.e(LOG_TAG, "" + e.getMessage(), e); } } @@ -690,18 +720,36 @@ public final class SelectionActionModeHelper { Preconditions.checkArgumentInRange(end, start, mText.length(), "end"); int[] wordIndices = getWordDelta(start, end); if (selection != null) { - mDelegate.logEvent(SelectionEvent.selectionModified( - wordIndices[0], wordIndices[1], selection)); + mLogger.logSelectionModifiedEvent( + wordIndices[0], wordIndices[1], selection); + // TODO: Remove the above legacy logging. + if (mClassificationSession != null) { + mClassificationSession.onSelectionEvent( + SelectionEvent.createSelectionModifiedEvent( + wordIndices[0], wordIndices[1], selection)); + } } else if (classification != null) { - mDelegate.logEvent(SelectionEvent.selectionModified( - wordIndices[0], wordIndices[1], classification)); + mLogger.logSelectionModifiedEvent( + wordIndices[0], wordIndices[1], classification); + // TODO: Remove the above legacy logging. + if (mClassificationSession != null) { + mClassificationSession.onSelectionEvent( + SelectionEvent.createSelectionModifiedEvent( + wordIndices[0], wordIndices[1], classification)); + } } else { - mDelegate.logEvent(SelectionEvent.selectionModified( - wordIndices[0], wordIndices[1])); + mLogger.logSelectionModifiedEvent( + wordIndices[0], wordIndices[1]); + // TODO: Remove the above legacy logging. + if (mClassificationSession != null) { + mClassificationSession.onSelectionEvent( + SelectionEvent.createSelectionModifiedEvent( + wordIndices[0], wordIndices[1])); + } } } catch (Exception e) { // Avoid crashes due to logging. - Log.d(LOG_TAG, e.getMessage()); + Log.e(LOG_TAG, "" + e.getMessage(), e); } } @@ -714,15 +762,27 @@ public final class SelectionActionModeHelper { Preconditions.checkArgumentInRange(end, start, mText.length(), "end"); int[] wordIndices = getWordDelta(start, end); if (classification != null) { - mDelegate.logEvent(SelectionEvent.selectionAction( - wordIndices[0], wordIndices[1], action, classification)); + mLogger.logSelectionActionEvent( + wordIndices[0], wordIndices[1], action, classification); + // TODO: Remove the above legacy logging. + if (mClassificationSession != null) { + mClassificationSession.onSelectionEvent( + SelectionEvent.createSelectionActionEvent( + wordIndices[0], wordIndices[1], action, classification)); + } } else { - mDelegate.logEvent(SelectionEvent.selectionAction( - wordIndices[0], wordIndices[1], action)); + mLogger.logSelectionActionEvent( + wordIndices[0], wordIndices[1], action); + // TODO: Remove the above legacy logging. + if (mClassificationSession != null) { + mClassificationSession.onSelectionEvent( + SelectionEvent.createSelectionActionEvent( + wordIndices[0], wordIndices[1], action)); + } } } catch (Exception e) { // Avoid crashes due to logging. - Log.d(LOG_TAG, e.getMessage()); + Log.e(LOG_TAG, "" + e.getMessage(), e); } } @@ -741,10 +801,10 @@ public final class SelectionActionModeHelper { wordIndices[0] = countWordsBackward(start); // For the selection start index, avoid counting a partial word backwards. - if (!mWordIterator.isBoundary(start) + if (!mTokenIterator.isBoundary(start) && !isWhitespace( - mWordIterator.preceding(start), - mWordIterator.following(start))) { + mTokenIterator.preceding(start), + mTokenIterator.following(start))) { // We counted a partial word. Remove it. wordIndices[0]--; } @@ -766,7 +826,7 @@ public final class SelectionActionModeHelper { int wordCount = 0; int offset = from; while (offset > mStartIndex) { - int start = mWordIterator.preceding(offset); + int start = mTokenIterator.preceding(offset); if (!isWhitespace(start, offset)) { wordCount++; } @@ -780,7 +840,7 @@ public final class SelectionActionModeHelper { int wordCount = 0; int offset = from; while (offset < mStartIndex) { - int end = mWordIterator.following(offset); + int end = mTokenIterator.following(offset); if (!isWhitespace(offset, end)) { wordCount++; } @@ -805,6 +865,7 @@ public final class SelectionActionModeHelper { private final int mTimeOutDuration; private final Supplier<SelectionResult> mSelectionResultSupplier; private final Consumer<SelectionResult> mSelectionResultCallback; + private final Supplier<SelectionResult> mTimeOutResultSupplier; private final TextView mTextView; private final String mOriginalText; @@ -813,16 +874,19 @@ public final class SelectionActionModeHelper { * @param timeOut time in milliseconds to timeout the query if it has not completed * @param selectionResultSupplier fetches the selection results. Runs on a background thread * @param selectionResultCallback receives the selection results. Runs on the UiThread + * @param timeOutResultSupplier default result if the task times out */ TextClassificationAsyncTask( @NonNull TextView textView, int timeOut, @NonNull Supplier<SelectionResult> selectionResultSupplier, - @NonNull Consumer<SelectionResult> selectionResultCallback) { + @NonNull Consumer<SelectionResult> selectionResultCallback, + @NonNull Supplier<SelectionResult> timeOutResultSupplier) { super(textView != null ? textView.getHandler() : null); mTextView = Preconditions.checkNotNull(textView); mTimeOutDuration = timeOut; mSelectionResultSupplier = Preconditions.checkNotNull(selectionResultSupplier); mSelectionResultCallback = Preconditions.checkNotNull(selectionResultCallback); + mTimeOutResultSupplier = Preconditions.checkNotNull(timeOutResultSupplier); // Make a copy of the original text. mOriginalText = getText(mTextView).toString(); } @@ -846,7 +910,7 @@ public final class SelectionActionModeHelper { private void onTimeOut() { if (getStatus() == Status.RUNNING) { - onPostExecute(null); + onPostExecute(mTimeOutResultSupplier.get()); } cancel(true); } @@ -861,8 +925,9 @@ public final class SelectionActionModeHelper { private static final int TRIM_DELTA = 120; // characters - private Context mContext; - private TextClassifier mTextClassifier; + private final Context mContext; + private final boolean mDarkLaunchEnabled; + private Supplier<TextClassifier> mTextClassifier; /** The original TextView text. **/ private String mText; @@ -871,9 +936,8 @@ public final class SelectionActionModeHelper { /** End index relative to mText. */ private int mSelectionEnd; - private final TextSelection.Options mSelectionOptions = new TextSelection.Options(); - private final TextClassification.Options mClassificationOptions = - new TextClassification.Options(); + @Nullable + private LocaleList mDefaultLocales; /** Trimmed text starting from mTrimStart in mText. */ private CharSequence mTrimmedText; @@ -894,24 +958,24 @@ public final class SelectionActionModeHelper { /** Whether the TextClassifier has been initialized. */ private boolean mHot; - TextClassificationHelper(Context context, TextClassifier textClassifier, + TextClassificationHelper(Context context, Supplier<TextClassifier> textClassifier, CharSequence text, int selectionStart, int selectionEnd, LocaleList locales) { - init(context, textClassifier, text, selectionStart, selectionEnd, locales); + init(textClassifier, text, selectionStart, selectionEnd, locales); + mContext = Preconditions.checkNotNull(context); + mDarkLaunchEnabled = TextClassificationManager.getSettings(mContext) + .isModelDarkLaunchEnabled(); } @UiThread - public void init(Context context, TextClassifier textClassifier, - CharSequence text, int selectionStart, int selectionEnd, LocaleList locales) { - mContext = Preconditions.checkNotNull(context); + public void init(Supplier<TextClassifier> textClassifier, CharSequence text, + int selectionStart, int selectionEnd, LocaleList locales) { mTextClassifier = Preconditions.checkNotNull(textClassifier); mText = Preconditions.checkNotNull(text).toString(); mLastClassificationText = null; // invalidate. Preconditions.checkArgument(selectionEnd > selectionStart); mSelectionStart = selectionStart; mSelectionEnd = selectionEnd; - mClassificationOptions.setDefaultLocales(locales); - mSelectionOptions.setDefaultLocales(locales) - .setDarkLaunchAllowed(true); + mDefaultLocales = locales; } @WorkerThread @@ -926,16 +990,19 @@ public final class SelectionActionModeHelper { trimText(); final TextSelection selection; if (mContext.getApplicationInfo().targetSdkVersion > Build.VERSION_CODES.O_MR1) { - selection = mTextClassifier.suggestSelection( - mTrimmedText, mRelativeStart, mRelativeEnd, mSelectionOptions); + final TextSelection.Request request = new TextSelection.Request.Builder( + mTrimmedText, mRelativeStart, mRelativeEnd) + .setDefaultLocales(mDefaultLocales) + .setDarkLaunchAllowed(true) + .build(); + selection = mTextClassifier.get().suggestSelection(request); } else { // Use old APIs. - selection = mTextClassifier.suggestSelection( - mTrimmedText, mRelativeStart, mRelativeEnd, - mSelectionOptions.getDefaultLocales()); + selection = mTextClassifier.get().suggestSelection( + mTrimmedText, mRelativeStart, mRelativeEnd, mDefaultLocales); } // Do not classify new selection boundaries if TextClassifier should be dark launched. - if (!mTextClassifier.getSettings().isDarkLaunch()) { + if (!mDarkLaunchEnabled) { mSelectionStart = Math.max(0, selection.getSelectionStartIndex() + mTrimStart); mSelectionEnd = Math.min( mText.length(), selection.getSelectionEndIndex() + mTrimStart); @@ -943,6 +1010,10 @@ public final class SelectionActionModeHelper { return performClassification(selection); } + public SelectionResult getOriginalSelection() { + return new SelectionResult(mSelectionStart, mSelectionEnd, null, null); + } + /** * Maximum time (in milliseconds) to wait for a textclassifier result before timing out. */ @@ -963,25 +1034,26 @@ public final class SelectionActionModeHelper { if (!Objects.equals(mText, mLastClassificationText) || mSelectionStart != mLastClassificationSelectionStart || mSelectionEnd != mLastClassificationSelectionEnd - || !Objects.equals( - mClassificationOptions.getDefaultLocales(), - mLastClassificationLocales)) { + || !Objects.equals(mDefaultLocales, mLastClassificationLocales)) { mLastClassificationText = mText; mLastClassificationSelectionStart = mSelectionStart; mLastClassificationSelectionEnd = mSelectionEnd; - mLastClassificationLocales = mClassificationOptions.getDefaultLocales(); + mLastClassificationLocales = mDefaultLocales; trimText(); final TextClassification classification; if (mContext.getApplicationInfo().targetSdkVersion > Build.VERSION_CODES.O_MR1) { - classification = mTextClassifier.classifyText( - mTrimmedText, mRelativeStart, mRelativeEnd, mClassificationOptions); + final TextClassification.Request request = + new TextClassification.Request.Builder( + mTrimmedText, mRelativeStart, mRelativeEnd) + .setDefaultLocales(mDefaultLocales) + .build(); + classification = mTextClassifier.get().classifyText(request); } else { // Use old APIs. - classification = mTextClassifier.classifyText( - mTrimmedText, mRelativeStart, mRelativeEnd, - mClassificationOptions.getDefaultLocales()); + classification = mTextClassifier.get().classifyText( + mTrimmedText, mRelativeStart, mRelativeEnd, mDefaultLocales); } mLastClassificationResult = new SelectionResult( mSelectionStart, mSelectionEnd, classification, selection); @@ -1005,14 +1077,14 @@ public final class SelectionActionModeHelper { private static final class SelectionResult { private final int mStart; private final int mEnd; - private final TextClassification mClassification; + @Nullable private final TextClassification mClassification; @Nullable private final TextSelection mSelection; SelectionResult(int start, int end, - TextClassification classification, @Nullable TextSelection selection) { + @Nullable TextClassification classification, @Nullable TextSelection selection) { mStart = start; mEnd = end; - mClassification = Preconditions.checkNotNull(classification); + mClassification = classification; mSelection = selection; } } @@ -1021,20 +1093,20 @@ public final class SelectionActionModeHelper { private static int getActionType(int menuItemId) { switch (menuItemId) { case TextView.ID_SELECT_ALL: - return SelectionEvent.ActionType.SELECT_ALL; + return SelectionEvent.ACTION_SELECT_ALL; case TextView.ID_CUT: - return SelectionEvent.ActionType.CUT; + return SelectionEvent.ACTION_CUT; case TextView.ID_COPY: - return SelectionEvent.ActionType.COPY; + return SelectionEvent.ACTION_COPY; case TextView.ID_PASTE: // fall through case TextView.ID_PASTE_AS_PLAIN_TEXT: - return SelectionEvent.ActionType.PASTE; + return SelectionEvent.ACTION_PASTE; case TextView.ID_SHARE: - return SelectionEvent.ActionType.SHARE; + return SelectionEvent.ACTION_SHARE; case TextView.ID_ASSIST: - return SelectionEvent.ActionType.SMART_SHARE; + return SelectionEvent.ACTION_SMART_SHARE; default: - return SelectionEvent.ActionType.OTHER; + return SelectionEvent.ACTION_OTHER; } } diff --git a/android/widget/SmartSelectSprite.java b/android/widget/SmartSelectSprite.java index a391c6ee..9a84f69d 100644 --- a/android/widget/SmartSelectSprite.java +++ b/android/widget/SmartSelectSprite.java @@ -26,7 +26,6 @@ import android.annotation.ColorInt; import android.annotation.FloatRange; import android.annotation.IntDef; import android.content.Context; -import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Path; @@ -36,7 +35,6 @@ import android.graphics.drawable.Drawable; import android.graphics.drawable.ShapeDrawable; import android.graphics.drawable.shapes.Shape; import android.text.Layout; -import android.util.TypedValue; import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; @@ -54,21 +52,15 @@ import java.util.List; final class SmartSelectSprite { private static final int EXPAND_DURATION = 300; - private static final int CORNER_DURATION = 150; - private static final float STROKE_WIDTH_DP = 1.5F; - - // GBLUE700 - @ColorInt - private static final int DEFAULT_STROKE_COLOR = 0xFF3367D6; + private static final int CORNER_DURATION = 50; private final Interpolator mExpandInterpolator; private final Interpolator mCornerInterpolator; - private final float mStrokeWidth; private Animator mActiveAnimator = null; private final Runnable mInvalidator; @ColorInt - private final int mStrokeColor; + private final int mFillColor; static final Comparator<RectF> RECTANGLE_COMPARATOR = Comparator .<RectF>comparingDouble(e -> e.bottom) @@ -124,26 +116,11 @@ final class SmartSelectSprite { return expansionDirection * -1; } - @Retention(SOURCE) - @IntDef({RectangleBorderType.FIT, RectangleBorderType.OVERSHOOT}) - private @interface RectangleBorderType { - /** A rectangle which, fully expanded, fits inside of its bounding rectangle. */ - int FIT = 0; - /** - * A rectangle which, when fully expanded, clips outside of its bounding rectangle so that - * its edges no longer appear rounded. - */ - int OVERSHOOT = 1; - } - - private final float mStrokeWidth; private final RectF mBoundingRectangle; private float mRoundRatio = 1.0f; private final @ExpansionDirection int mExpansionDirection; - private final @RectangleBorderType int mRectangleBorderType; private final RectF mDrawRect = new RectF(); - private final RectF mClipRect = new RectF(); private final Path mClipPath = new Path(); /** How offset the left edge of the rectangle is from the left side of the bounding box. */ @@ -159,13 +136,9 @@ final class SmartSelectSprite { private RoundedRectangleShape( final RectF boundingRectangle, final @ExpansionDirection int expansionDirection, - final @RectangleBorderType int rectangleBorderType, - final boolean inverted, - final float strokeWidth) { + final boolean inverted) { mBoundingRectangle = new RectF(boundingRectangle); mBoundingWidth = boundingRectangle.width(); - mRectangleBorderType = rectangleBorderType; - mStrokeWidth = strokeWidth; mInverted = inverted && expansionDirection != ExpansionDirection.CENTER; if (inverted) { @@ -182,14 +155,8 @@ final class SmartSelectSprite { } /* - * In order to achieve the "rounded rectangle hits the wall" effect, the drawing needs to be - * done in two passes. In this context, the wall is the bounding rectangle and in the first - * pass we need to draw the rounded rectangle (expanded and with a corner radius as per - * object properties) clipped by the bounding box. If the rounded rectangle expands outside - * of the bounding box, one more pass needs to be done, as there will now be a hole in the - * rounded rectangle where it "flattened" against the bounding box. In order to fill just - * this hole, we need to draw the bounding box, but clip it with the rounded rectangle and - * this will connect the missing pieces. + * In order to achieve the "rounded rectangle hits the wall" effect, we draw an expanding + * rounded rectangle that is clipped by the bounding box of the selected text. */ @Override public void draw(Canvas canvas, Paint paint) { @@ -201,31 +168,8 @@ final class SmartSelectSprite { final float adjustedCornerRadius = getAdjustedCornerRadius(); mDrawRect.set(mBoundingRectangle); - mDrawRect.left = mBoundingRectangle.left + mLeftBoundary; - mDrawRect.right = mBoundingRectangle.left + mRightBoundary; - - if (mRectangleBorderType == RectangleBorderType.OVERSHOOT) { - mDrawRect.left -= cornerRadius / 2; - mDrawRect.right += cornerRadius / 2; - } else { - switch (mExpansionDirection) { - case ExpansionDirection.CENTER: - break; - case ExpansionDirection.LEFT: - mDrawRect.right += cornerRadius; - break; - case ExpansionDirection.RIGHT: - mDrawRect.left -= cornerRadius; - break; - } - } - - canvas.save(); - mClipRect.set(mBoundingRectangle); - mClipRect.inset(-mStrokeWidth / 2, -mStrokeWidth / 2); - canvas.clipRect(mClipRect); - canvas.drawRoundRect(mDrawRect, adjustedCornerRadius, adjustedCornerRadius, paint); - canvas.restore(); + mDrawRect.left = mBoundingRectangle.left + mLeftBoundary - cornerRadius / 2; + mDrawRect.right = mBoundingRectangle.left + mRightBoundary + cornerRadius / 2; canvas.save(); mClipPath.reset(); @@ -272,11 +216,7 @@ final class SmartSelectSprite { } private float getBoundingWidth() { - if (mRectangleBorderType == RectangleBorderType.OVERSHOOT) { - return (int) (mBoundingRectangle.width() + getCornerRadius()); - } else { - return mBoundingRectangle.width(); - } + return (int) (mBoundingRectangle.width() + getCornerRadius()); } } @@ -388,19 +328,20 @@ final class SmartSelectSprite { } /** - * @param context the {@link Context} in which the animation will run + * @param context the {@link Context} in which the animation will run + * @param highlightColor the highlight color of the underlying {@link TextView} * @param invalidator a {@link Runnable} which will be called every time the animation updates, * indicating that the view drawing the animation should invalidate itself */ - SmartSelectSprite(final Context context, final Runnable invalidator) { + SmartSelectSprite(final Context context, @ColorInt int highlightColor, + final Runnable invalidator) { mExpandInterpolator = AnimationUtils.loadInterpolator( context, android.R.interpolator.fast_out_slow_in); mCornerInterpolator = AnimationUtils.loadInterpolator( context, android.R.interpolator.fast_out_linear_in); - mStrokeWidth = dpToPixel(context, STROKE_WIDTH_DP); - mStrokeColor = getStrokeColor(context); + mFillColor = highlightColor; mInvalidator = Preconditions.checkNotNull(invalidator); } @@ -437,17 +378,14 @@ final class SmartSelectSprite { RectangleWithTextSelectionLayout centerRectangle = null; int startingOffset = 0; - int startingRectangleIndex = 0; - for (int index = 0; index < rectangleCount; ++index) { - final RectangleWithTextSelectionLayout rectangleWithTextSelectionLayout = - destinationRectangles.get(index); + for (RectangleWithTextSelectionLayout rectangleWithTextSelectionLayout : + destinationRectangles) { final RectF rectangle = rectangleWithTextSelectionLayout.getRectangle(); if (contains(rectangle, start)) { centerRectangle = rectangleWithTextSelectionLayout; break; } startingOffset += rectangle.width(); - ++startingRectangleIndex; } if (centerRectangle == null) { @@ -459,9 +397,6 @@ final class SmartSelectSprite { final @RoundedRectangleShape.ExpansionDirection int[] expansionDirections = generateDirections(centerRectangle, destinationRectangles); - final @RoundedRectangleShape.RectangleBorderType int[] rectangleBorderTypes = - generateBorderTypes(rectangleCount); - for (int index = 0; index < rectangleCount; ++index) { final RectangleWithTextSelectionLayout rectangleWithTextSelectionLayout = destinationRectangles.get(index); @@ -469,10 +404,8 @@ final class SmartSelectSprite { final RoundedRectangleShape shape = new RoundedRectangleShape( rectangle, expansionDirections[index], - rectangleBorderTypes[index], rectangleWithTextSelectionLayout.getTextSelectionLayout() - == Layout.TEXT_SELECTION_LAYOUT_RIGHT_TO_LEFT, - mStrokeWidth); + == Layout.TEXT_SELECTION_LAYOUT_RIGHT_TO_LEFT); cornerAnimators.add(createCornerAnimator(shape, updateListener)); shapes.add(shape); } @@ -480,44 +413,23 @@ final class SmartSelectSprite { final RectangleList rectangleList = new RectangleList(shapes); final ShapeDrawable shapeDrawable = new ShapeDrawable(rectangleList); - final float startingOffsetLeft; - final float startingOffsetRight; - - final RoundedRectangleShape startingRectangleShape = shapes.get(startingRectangleIndex); - final float cornerRadius = startingRectangleShape.getCornerRadius(); - if (startingRectangleShape.mRectangleBorderType - == RoundedRectangleShape.RectangleBorderType.FIT) { - switch (startingRectangleShape.mExpansionDirection) { - case RoundedRectangleShape.ExpansionDirection.LEFT: - startingOffsetLeft = startingOffsetRight = startingOffset - cornerRadius / 2; - break; - case RoundedRectangleShape.ExpansionDirection.RIGHT: - startingOffsetLeft = startingOffsetRight = startingOffset + cornerRadius / 2; - break; - case RoundedRectangleShape.ExpansionDirection.CENTER: // fall through - default: - startingOffsetLeft = startingOffset - cornerRadius / 2; - startingOffsetRight = startingOffset + cornerRadius / 2; - break; - } - } else { - startingOffsetLeft = startingOffsetRight = startingOffset; - } - final Paint paint = shapeDrawable.getPaint(); - paint.setColor(mStrokeColor); - paint.setStyle(Paint.Style.STROKE); - paint.setStrokeWidth(mStrokeWidth); + paint.setColor(mFillColor); + paint.setStyle(Paint.Style.FILL); mExistingRectangleList = rectangleList; mExistingDrawable = shapeDrawable; - mActiveAnimator = createAnimator(rectangleList, startingOffsetLeft, startingOffsetRight, - cornerAnimators, updateListener, - onAnimationEnd); + mActiveAnimator = createAnimator(rectangleList, startingOffset, startingOffset, + cornerAnimators, updateListener, onAnimationEnd); mActiveAnimator.start(); } + /** Returns whether the sprite is currently animating. */ + public boolean isAnimationActive() { + return mActiveAnimator != null && mActiveAnimator.isRunning(); + } + private Animator createAnimator( final RectangleList rectangleList, final float startingOffsetLeft, @@ -625,36 +537,6 @@ final class SmartSelectSprite { return result; } - private static @RoundedRectangleShape.RectangleBorderType int[] generateBorderTypes( - final int numberOfRectangles) { - final @RoundedRectangleShape.RectangleBorderType int[] result = new int[numberOfRectangles]; - - for (int i = 1; i < result.length - 1; ++i) { - result[i] = RoundedRectangleShape.RectangleBorderType.OVERSHOOT; - } - - result[0] = RoundedRectangleShape.RectangleBorderType.FIT; - result[result.length - 1] = RoundedRectangleShape.RectangleBorderType.FIT; - return result; - } - - private static float dpToPixel(final Context context, final float dp) { - return TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, - dp, - context.getResources().getDisplayMetrics()); - } - - @ColorInt - private static int getStrokeColor(final Context context) { - final TypedValue typedValue = new TypedValue(); - final TypedArray array = context.obtainStyledAttributes(typedValue.data, new int[]{ - android.R.attr.colorControlActivated}); - final int result = array.getColor(0, DEFAULT_STROKE_COLOR); - array.recycle(); - return result; - } - /** * A variant of {@link RectF#contains(float, float)} that also allows the point to reside on * the right boundary of the rectangle. diff --git a/android/widget/TextInputTimePickerView.java b/android/widget/TextInputTimePickerView.java index 0cf8faad..e0261ad0 100644 --- a/android/widget/TextInputTimePickerView.java +++ b/android/widget/TextInputTimePickerView.java @@ -174,7 +174,8 @@ public class TextInputTimePickerView extends RelativeLayout { */ void updateTextInputValues(int localizedHour, int minute, int amOrPm, boolean is24Hour, boolean hourFormatStartsAtZero) { - final String format = "%d"; + final String hourFormat = "%d"; + final String minuteFormat = "%02d"; mIs24Hour = is24Hour; mHourFormatStartsAtZero = hourFormatStartsAtZero; @@ -187,8 +188,8 @@ public class TextInputTimePickerView extends RelativeLayout { mAmPmSpinner.setSelection(1); } - mHourEditText.setText(String.format(format, localizedHour)); - mMinuteEditText.setText(String.format(format, minute)); + mHourEditText.setText(String.format(hourFormat, localizedHour)); + mMinuteEditText.setText(String.format(minuteFormat, minute)); if (mErrorShowing) { validateInput(); diff --git a/android/widget/TextView.java b/android/widget/TextView.java index 7d3fcf46..11db6b65 100644 --- a/android/widget/TextView.java +++ b/android/widget/TextView.java @@ -36,6 +36,7 @@ import android.annotation.StringRes; import android.annotation.StyleRes; import android.annotation.XmlRes; import android.app.Activity; +import android.app.PendingIntent; import android.app.assist.AssistStructure; import android.content.ClipData; import android.content.ClipDescription; @@ -80,8 +81,8 @@ import android.text.GraphicsOperations; import android.text.InputFilter; import android.text.InputType; import android.text.Layout; -import android.text.MeasuredText; import android.text.ParcelableSpan; +import android.text.PrecomputedText; import android.text.Selection; import android.text.SpanWatcher; import android.text.Spannable; @@ -162,6 +163,8 @@ import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; +import android.view.textclassifier.TextClassification; +import android.view.textclassifier.TextClassificationContext; import android.view.textclassifier.TextClassificationManager; import android.view.textclassifier.TextClassifier; import android.view.textclassifier.TextLinks; @@ -187,6 +190,10 @@ import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Arrays; import java.util.Locale; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.function.Supplier; /** * A user interface element that displays text to the user. @@ -291,6 +298,7 @@ import java.util.Locale; * @attr ref android.R.styleable#TextView_drawableTintMode * @attr ref android.R.styleable#TextView_lineSpacingExtra * @attr ref android.R.styleable#TextView_lineSpacingMultiplier + * @attr ref android.R.styleable#TextView_justificationMode * @attr ref android.R.styleable#TextView_marqueeRepeatLimit * @attr ref android.R.styleable#TextView_inputType * @attr ref android.R.styleable#TextView_imeOptions @@ -319,6 +327,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener // Enum for the "typeface" XML parameter. // TODO: How can we get this from the XML instead of hardcoding it here? + /** @hide */ + @IntDef(value = {DEFAULT_TYPEFACE, SANS, SERIF, MONOSPACE}) + @Retention(RetentionPolicy.SOURCE) + public @interface XMLTypefaceAttr{} + private static final int DEFAULT_TYPEFACE = -1; private static final int SANS = 1; private static final int SERIF = 2; private static final int MONOSPACE = 3; @@ -416,6 +429,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private boolean mPreDrawListenerDetached; private TextClassifier mTextClassifier; + private TextClassifier mTextClassificationSession; // A flag to prevent repeated movements from escaping the enclosing text view. The idea here is // that if a user is holding down a movement key to traverse text, we shouldn't also traverse @@ -633,8 +647,12 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener */ private Layout mSavedMarqueeModeLayout; + // Do not update following mText/mSpannable/mPrecomputed except for setTextInternal() @ViewDebug.ExportedProperty(category = "text") - private CharSequence mText; + private @Nullable CharSequence mText; + private @Nullable Spannable mSpannable; + private @Nullable PrecomputedText mPrecomputed; + private CharSequence mTransformed; private BufferType mBufferType = BufferType.NORMAL; @@ -791,11 +809,18 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener // mAutoSizeStepGranularityInPx. private boolean mHasPresetAutoSizeValues = false; + // Autofill-related attributes + // // Indicates whether the text was set statically or dynamically, so it can be used to // sanitize autofill requests. private boolean mTextSetFromXmlOrResourceId = false; - // Resource id used to set the text - used for autofill purposes. + // Resource id used to set the text. private @StringRes int mTextId = ResourceId.ID_NULL; + // Last value used on AFM.notifyValueChanged(), used to optimize autofill workflow by avoiding + // calls when the value did not change + private CharSequence mLastValueSentToAutofillManager; + // + // End of autofill-related attributes /** * Kick-start the font cache for the zygote process (to pay the cost of @@ -856,7 +881,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener setImportantForAutofill(IMPORTANT_FOR_AUTOFILL_YES); } - mText = ""; + setTextInternal(""); final Resources res = getResources(); final CompatibilityInfo compat = res.getCompatibilityInfo(); @@ -1597,6 +1622,13 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } + // Update mText and mPrecomputed + private void setTextInternal(@Nullable CharSequence text) { + mText = text; + mSpannable = (text instanceof Spannable) ? (Spannable) text : null; + mPrecomputed = (text instanceof PrecomputedText) ? (PrecomputedText) text : null; + } + /** * Specify whether this widget should automatically scale the text to try to perfectly fit * within the layout bounds by using the default auto-size configuration. @@ -1905,19 +1937,12 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener // Calculate the sizes set based on minimum size, maximum size and step size if we do // not have a predefined set of sizes or if the current sizes array is empty. if (!mHasPresetAutoSizeValues || mAutoSizeTextSizesInPx.length == 0) { - int autoSizeValuesLength = 1; - float currentSize = Math.round(mAutoSizeMinTextSizeInPx); - while (Math.round(currentSize + mAutoSizeStepGranularityInPx) - <= Math.round(mAutoSizeMaxTextSizeInPx)) { - autoSizeValuesLength++; - currentSize += mAutoSizeStepGranularityInPx; - } - - int[] autoSizeTextSizesInPx = new int[autoSizeValuesLength]; - float sizeToAdd = mAutoSizeMinTextSizeInPx; + final int autoSizeValuesLength = ((int) Math.floor((mAutoSizeMaxTextSizeInPx + - mAutoSizeMinTextSizeInPx) / mAutoSizeStepGranularityInPx)) + 1; + final int[] autoSizeTextSizesInPx = new int[autoSizeValuesLength]; for (int i = 0; i < autoSizeValuesLength; i++) { - autoSizeTextSizesInPx[i] = Math.round(sizeToAdd); - sizeToAdd += mAutoSizeStepGranularityInPx; + autoSizeTextSizesInPx[i] = Math.round( + mAutoSizeMinTextSizeInPx + (i * mAutoSizeStepGranularityInPx)); } mAutoSizeTextSizesInPx = cleanupAutoSizePresetSizes(autoSizeTextSizesInPx); } @@ -1962,40 +1987,59 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } } - } else if (mText instanceof Spannable) { + } else if (mSpannable != null) { // Reset the selection. - Selection.setSelection((Spannable) mText, getSelectionEnd()); + Selection.setSelection(mSpannable, getSelectionEnd()); } } } - private void setTypefaceFromAttrs(Typeface fontTypeface, String familyName, int typefaceIndex, - int styleIndex) { - Typeface tf = fontTypeface; - if (tf == null && familyName != null) { - tf = Typeface.create(familyName, styleIndex); - } else if (tf != null && tf.getStyle() != styleIndex) { - tf = Typeface.create(tf, styleIndex); - } - if (tf != null) { - setTypeface(tf); - return; + /** + * Sets the Typeface taking into account the given attributes. + * + * @param typeface a typeface + * @param familyName family name string, e.g. "serif" + * @param typefaceIndex an index of the typeface enum, e.g. SANS, SERIF. + * @param style a typeface style + * @param weight a weight value for the Typeface or -1 if not specified. + */ + private void setTypefaceFromAttrs(@Nullable Typeface typeface, @Nullable String familyName, + @XMLTypefaceAttr int typefaceIndex, @Typeface.Style int style, + @IntRange(from = -1, to = Typeface.MAX_WEIGHT) int weight) { + if (typeface == null && familyName != null) { + // Lookup normal Typeface from system font map. + final Typeface normalTypeface = Typeface.create(familyName, Typeface.NORMAL); + resolveStyleAndSetTypeface(normalTypeface, style, weight); + } else if (typeface != null) { + resolveStyleAndSetTypeface(typeface, style, weight); + } else { // both typeface and familyName is null. + switch (typefaceIndex) { + case SANS: + resolveStyleAndSetTypeface(Typeface.SANS_SERIF, style, weight); + break; + case SERIF: + resolveStyleAndSetTypeface(Typeface.SERIF, style, weight); + break; + case MONOSPACE: + resolveStyleAndSetTypeface(Typeface.MONOSPACE, style, weight); + break; + case DEFAULT_TYPEFACE: + default: + resolveStyleAndSetTypeface(null, style, weight); + break; + } } - switch (typefaceIndex) { - case SANS: - tf = Typeface.SANS_SERIF; - break; - - case SERIF: - tf = Typeface.SERIF; - break; + } - case MONOSPACE: - tf = Typeface.MONOSPACE; - break; + private void resolveStyleAndSetTypeface(@NonNull Typeface typeface, @Typeface.Style int style, + @IntRange(from = -1, to = Typeface.MAX_WEIGHT) int weight) { + if (weight >= 0) { + weight = Math.min(Typeface.MAX_WEIGHT, weight); + final boolean italic = (style & Typeface.ITALIC) != 0; + setTypeface(Typeface.create(typeface, weight, italic)); + } else { + setTypeface(typeface, style); } - - setTypeface(tf, styleIndex); } private void setRelativeDrawablesIfNeeded(Drawable start, Drawable end) { @@ -2080,7 +2124,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * @attr ref android.R.styleable#TextView_typeface * @attr ref android.R.styleable#TextView_textStyle */ - public void setTypeface(Typeface tf, int style) { + public void setTypeface(@Nullable Typeface tf, @Typeface.Style int style) { if (style > 0) { if (tf == null) { tf = Typeface.defaultFromStyle(style); @@ -2329,7 +2373,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (mMovement != movement) { mMovement = movement; - if (movement != null && !(mText instanceof Spannable)) { + if (movement != null && mSpannable == null) { setText(mText); } @@ -2379,8 +2423,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return; } if (mTransformation != null) { - if (mText instanceof Spannable) { - ((Spannable) mText).removeSpan(mTransformation); + if (mSpannable != null) { + mSpannable.removeSpan(mTransformation); } } @@ -2397,7 +2441,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener setText(mText); if (hasPasswordTransformationMethod()) { - notifyAccessibilityStateChanged( + notifyViewAccessibilityStateChangedIfNeeded( AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED); } @@ -3385,6 +3429,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener boolean mFontFamilyExplicit = false; int mTypefaceIndex = -1; int mStyleIndex = -1; + int mFontWeight = -1; boolean mAllCaps = false; int mShadowColor = 0; float mShadowDx = 0, mShadowDy = 0, mShadowRadius = 0; @@ -3409,6 +3454,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener + " mFontFamilyExplicit:" + mFontFamilyExplicit + "\n" + " mTypefaceIndex:" + mTypefaceIndex + "\n" + " mStyleIndex:" + mStyleIndex + "\n" + + " mFontWeight:" + mFontWeight + "\n" + " mAllCaps:" + mAllCaps + "\n" + " mShadowColor:" + mShadowColor + "\n" + " mShadowDx:" + mShadowDx + "\n" @@ -3444,6 +3490,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener com.android.internal.R.styleable.TextAppearance_fontFamily); sAppearanceValues.put(com.android.internal.R.styleable.TextView_textStyle, com.android.internal.R.styleable.TextAppearance_textStyle); + sAppearanceValues.put(com.android.internal.R.styleable.TextView_textFontWeight, + com.android.internal.R.styleable.TextAppearance_textFontWeight); sAppearanceValues.put(com.android.internal.R.styleable.TextView_textAllCaps, com.android.internal.R.styleable.TextAppearance_textAllCaps); sAppearanceValues.put(com.android.internal.R.styleable.TextView_shadowColor, @@ -3529,6 +3577,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener case com.android.internal.R.styleable.TextAppearance_textStyle: attributes.mStyleIndex = appearance.getInt(attr, attributes.mStyleIndex); break; + case com.android.internal.R.styleable.TextAppearance_textFontWeight: + attributes.mFontWeight = appearance.getInt(attr, attributes.mFontWeight); + break; case com.android.internal.R.styleable.TextAppearance_textAllCaps: attributes.mAllCaps = appearance.getBoolean(attr, attributes.mAllCaps); break; @@ -3591,7 +3642,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener attributes.mFontFamily = null; } setTypefaceFromAttrs(attributes.mFontTypeface, attributes.mFontFamily, - attributes.mTypefaceIndex, attributes.mStyleIndex); + attributes.mTypefaceIndex, attributes.mStyleIndex, attributes.mFontWeight); if (attributes.mShadowColor != 0) { setShadowLayer(attributes.mShadowRadius, attributes.mShadowDx, attributes.mShadowDy, @@ -3858,7 +3909,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * @attr ref android.R.styleable#TextView_typeface * @attr ref android.R.styleable#TextView_textStyle */ - public void setTypeface(Typeface tf) { + public void setTypeface(@Nullable Typeface tf) { if (mTextPaint.getTypeface() != tf) { mTextPaint.setTypeface(tf); @@ -4085,6 +4136,36 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } /** + * Gets the parameters for text layout precomputation, for use with {@link PrecomputedText}. + * + * @return a current {@link PrecomputedText.Params} + * @see PrecomputedText + */ + public @NonNull PrecomputedText.Params getTextMetricsParams() { + return new PrecomputedText.Params(new TextPaint(mTextPaint), getTextDirectionHeuristic(), + mBreakStrategy, mHyphenationFrequency); + } + + /** + * Apply the text layout parameter. + * + * Update the TextView parameters to be compatible with {@link PrecomputedText.Params}. + * @see PrecomputedText + */ + public void setTextMetricsParams(@NonNull PrecomputedText.Params params) { + mTextPaint.set(params.getTextPaint()); + mUserSetTextScaleX = true; + mTextDir = params.getTextDirection(); + mBreakStrategy = params.getBreakStrategy(); + mHyphenationFrequency = params.getHyphenationFrequency(); + if (mLayout != null) { + nullLayouts(); + requestLayout(); + invalidate(); + } + } + + /** * Set justification mode. The default value is {@link Layout#JUSTIFICATION_MODE_NONE}. If the * last line is too short for justification, the last line will be displayed with the * alignment set by {@link android.view.View#setTextAlignment}. @@ -5152,7 +5233,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener public void setAccessibilityHeading(boolean isHeading) { if (isHeading != mIsAccessibilityHeading) { mIsAccessibilityHeading = isHeading; - notifyAccessibilityStateChanged(AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED); + notifyViewAccessibilityStateChangedIfNeeded( + AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED); } } @@ -5186,7 +5268,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener ((Editable) mText).append(text, start, end); if (mAutoLinkMask != 0) { - boolean linksWereAdded = Linkify.addLinks((Spannable) mText, mAutoLinkMask); + boolean linksWereAdded = Linkify.addLinks(mSpannable, mAutoLinkMask); // Do not change the movement method for text that support text selection as it // would prevent an arbitrary cursor displacement. if (linksWereAdded && mLinksClickable && !textCanBeSelected()) { @@ -5345,7 +5427,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } if (ss.selStart >= 0 && ss.selEnd >= 0) { - if (mText instanceof Spannable) { + if (mSpannable != null) { int len = mText.length(); if (ss.selStart > len || ss.selEnd > len) { @@ -5358,7 +5440,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener Log.e(LOG_TAG, "Saved cursor position " + ss.selStart + "/" + ss.selEnd + " out of range for " + restored + "text " + mText); } else { - Selection.setSelection((Spannable) mText, ss.selStart, ss.selEnd); + Selection.setSelection(mSpannable, ss.selStart, ss.selEnd); if (ss.frozenWithFocus) { createEditorIfNeeded(); @@ -5460,9 +5542,16 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * {@link android.text.Editable.Factory} to create final or intermediate * {@link Editable Editables}. * + * If the passed text is a {@link PrecomputedText} but the parameters used to create the + * PrecomputedText mismatches with this TextView, IllegalArgumentException is thrown. To ensure + * the parameters match, you can call {@link TextView#setTextMetricsParams} before calling this. + * * @param text text to be displayed * * @attr ref android.R.styleable#TextView_text + * @throws IllegalArgumentException if the passed text is a {@link PrecomputedText} but the + * parameters used to create the PrecomputedText mismatches + * with this TextView. */ @android.view.RemotableViewMethod public final void setText(CharSequence text) { @@ -5565,6 +5654,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener needEditableForNotification = true; } + PrecomputedText precomputed = + (text instanceof PrecomputedText) ? (PrecomputedText) text : null; if (type == BufferType.EDITABLE || getKeyListener() != null || needEditableForNotification) { createEditorIfNeeded(); @@ -5574,9 +5665,22 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener setFilters(t, mFilters); InputMethodManager imm = InputMethodManager.peekInstance(); if (imm != null) imm.restartInput(this); + } else if (precomputed != null) { + if (mTextDir == null) { + mTextDir = getTextDirectionHeuristic(); + } + if (!precomputed.getParams().isSameTextMetricsInternal( + getPaint(), mTextDir, mBreakStrategy, mHyphenationFrequency)) { + throw new IllegalArgumentException( + "PrecomputedText's Parameters don't match the parameters of this TextView." + + "Consider using setTextMetricsParams(precomputedText.getParams()) " + + "to override the settings of this TextView: " + + "PrecomputedText: " + precomputed.getParams() + + "TextView: " + getTextMetricsParams()); + } } else if (type == BufferType.SPANNABLE || mMovement != null) { text = mSpannableFactory.newSpannable(text); - } else if (!(text instanceof MeasuredText || text instanceof CharWrapper)) { + } else if (!(text instanceof CharWrapper)) { text = TextUtils.stringOrSpannedString(text); } @@ -5598,7 +5702,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * movement method, because setMovementMethod() may call * setText() again to try to upgrade the buffer type. */ - mText = text; + setTextInternal(text); // Do not change the movement method for text that support text selection as it // would prevent an arbitrary cursor displacement. @@ -5609,7 +5713,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } mBufferType = type; - mText = text; + setTextInternal(text); if (mTransformation == null) { mTransformed = text; @@ -5659,12 +5763,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener sendOnTextChanged(text, 0, oldlen, textLength); onTextChanged(text, 0, oldlen, textLength); - notifyAccessibilityStateChanged(AccessibilityEvent.CONTENT_CHANGE_TYPE_TEXT); + notifyViewAccessibilityStateChangedIfNeeded(AccessibilityEvent.CONTENT_CHANGE_TYPE_TEXT); if (needEditableForNotification) { sendAfterTextChanged((Editable) text); } else { - // Always notify AutoFillManager - it will return right away if autofill is disabled. notifyAutoFillManagerAfterTextChangedIfNeeded(); } @@ -5736,8 +5839,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener setText(text, type); if (start >= 0 || end >= 0) { - if (mText instanceof Spannable) { - Selection.setSelection((Spannable) mText, + if (mSpannable != null) { + Selection.setSelection(mSpannable, Math.max(0, Math.min(start, len)), Math.max(0, Math.min(end, len))); } @@ -5902,15 +6005,19 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener boolean forceUpdate = false; if (isPassword) { setTransformationMethod(PasswordTransformationMethod.getInstance()); - setTypefaceFromAttrs(null/* fontTypeface */, null /* fontFamily */, MONOSPACE, 0); + setTypefaceFromAttrs(null/* fontTypeface */, null /* fontFamily */, MONOSPACE, + Typeface.NORMAL, -1 /* weight, not specifeid */); } else if (isVisiblePassword) { if (mTransformation == PasswordTransformationMethod.getInstance()) { forceUpdate = true; } - setTypefaceFromAttrs(null/* fontTypeface */, null /* fontFamily */, MONOSPACE, 0); + setTypefaceFromAttrs(null/* fontTypeface */, null /* fontFamily */, MONOSPACE, + Typeface.NORMAL, -1 /* weight, not specified */); } else if (wasPassword || wasVisiblePassword) { // not in password mode, clean up typeface and transformation - setTypefaceFromAttrs(null/* fontTypeface */, null /* fontFamily */, -1, -1); + setTypefaceFromAttrs(null/* fontTypeface */, null /* fontFamily */, + DEFAULT_TYPEFACE /* typeface index */, Typeface.NORMAL, + -1 /* weight, not specified */); if (mTransformation == PasswordTransformationMethod.getInstance()) { forceUpdate = true; } @@ -5927,7 +6034,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } if (!isSuggestionsEnabled()) { - mText = removeSuggestionSpans(mText); + setTextInternal(removeSuggestionSpans(mText)); } InputMethodManager imm = InputMethodManager.peekInstance(); @@ -6393,7 +6500,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener public void setError(CharSequence error, Drawable icon) { createEditorIfNeeded(); mEditor.setError(error, icon); - notifyAccessibilityStateChanged( + notifyViewAccessibilityStateChangedIfNeeded( AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED); } @@ -6855,8 +6962,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener public boolean hasOverlappingRendering() { // horizontal fading edge causes SaveLayerAlpha, which doesn't support alpha modulation return ((getBackground() != null && getBackground().getCurrent() != null) - || mText instanceof Spannable || hasSelection() - || isHorizontalFadingEdgeEnabled()); + || mSpannable != null || hasSelection() || isHorizontalFadingEdgeEnabled()); } /** @@ -6958,9 +7064,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener final int selEnd = getSelectionEnd(); if (mMovement != null && (isFocused() || isPressed()) && selStart >= 0) { if (selStart == selEnd) { - if (mEditor != null && mEditor.isCursorVisible() - && (SystemClock.uptimeMillis() - mEditor.mShowCursor) - % (2 * Editor.BLINK) < Editor.BLINK) { + if (mEditor != null && mEditor.shouldRenderCursor()) { if (mHighlightPathBogus) { if (mHighlightPath == null) mHighlightPath = new Path(); mHighlightPath.reset(); @@ -7308,11 +7412,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener @Override public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) { - if (mText instanceof Spannable && mLinksClickable) { + if (mSpannable != null && mLinksClickable) { final float x = event.getX(pointerIndex); final float y = event.getY(pointerIndex); final int offset = getOffsetForPosition(x, y); - final ClickableSpan[] clickables = ((Spannable) mText).getSpans(offset, offset, + final ClickableSpan[] clickables = mSpannable.getSpans(offset, offset, ClickableSpan.class); if (clickables.length > 0) { return PointerIcon.getSystemIcon(mContext, PointerIcon.TYPE_HAND); @@ -7405,10 +7509,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } else if (which == KEY_DOWN_HANDLED_BY_MOVEMENT_METHOD) { // mMovement is not null from doKeyDown - mMovement.onKeyUp(this, (Spannable) mText, keyCode, up); + mMovement.onKeyUp(this, mSpannable, keyCode, up); while (--repeatCount > 0) { - mMovement.onKeyDown(this, (Spannable) mText, keyCode, down); - mMovement.onKeyUp(this, (Spannable) mText, keyCode, up); + mMovement.onKeyDown(this, mSpannable, keyCode, down); + mMovement.onKeyUp(this, mSpannable, keyCode, up); } } @@ -7603,8 +7707,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener boolean doDown = true; if (otherEvent != null) { try { - boolean handled = mMovement.onKeyOther(this, (Spannable) mText, - otherEvent); + boolean handled = mMovement.onKeyOther(this, mSpannable, otherEvent); doDown = false; if (handled) { return KEY_EVENT_HANDLED; @@ -7615,7 +7718,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } if (doDown) { - if (mMovement.onKeyDown(this, (Spannable) mText, keyCode, event)) { + if (mMovement.onKeyDown(this, mSpannable, keyCode, event)) { if (event.getRepeatCount() == 0 && !KeyEvent.isModifierKey(keyCode)) { mPreventDefaultMovement = true; } @@ -7757,7 +7860,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } if (mMovement != null && mLayout != null) { - if (mMovement.onKeyUp(this, (Spannable) mText, keyCode, event)) { + if (mMovement.onKeyUp(this, mSpannable, keyCode, event)) { return true; } } @@ -7993,7 +8096,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return false; } - private void nullLayouts() { + /** @hide */ + @VisibleForTesting + public void nullLayouts() { if (mLayout instanceof BoringLayout && mSavedLayout == null) { mSavedLayout = (BoringLayout) mLayout; } @@ -8087,7 +8192,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * not the full view width with padding. * {@hide} */ - protected void makeNewLayout(int wantWidth, int hintWidth, + @VisibleForTesting + public void makeNewLayout(int wantWidth, int hintWidth, BoringLayout.Metrics boring, BoringLayout.Metrics hintBoring, int ellipsisWidth, boolean bringIntoView) { @@ -8220,13 +8326,23 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } /** + * Returns true if DynamicLayout is required + * + * @hide + */ + @VisibleForTesting + public boolean useDynamicLayout() { + return isTextSelectable() || (mSpannable != null && mPrecomputed == null); + } + + /** * @hide */ protected Layout makeSingleLayout(int wantWidth, BoringLayout.Metrics boring, int ellipsisWidth, Layout.Alignment alignment, boolean shouldEllipsize, TruncateAt effectiveEllipsize, boolean useSaved) { Layout result = null; - if (mText instanceof Spannable) { + if (useDynamicLayout()) { final DynamicLayout.Builder builder = DynamicLayout.Builder.obtain(mText, mTextPaint, wantWidth) .setDisplayText(mTransformed) @@ -8377,7 +8493,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return mIncludePad; } - private static final BoringLayout.Metrics UNKNOWN_BORING = new BoringLayout.Metrics(); + /** @hide */ + @VisibleForTesting + public static final BoringLayout.Metrics UNKNOWN_BORING = new BoringLayout.Metrics(); @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { @@ -9166,7 +9284,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } if (newStart != start) { - Selection.setSelection((Spannable) mText, newStart); + Selection.setSelection(mSpannable, newStart); return true; } @@ -9696,11 +9814,21 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return; } final AutofillManager afm = mContext.getSystemService(AutofillManager.class); - if (afm != null) { + if (afm == null) { + return; + } + + if (mLastValueSentToAutofillManager == null + || !mLastValueSentToAutofillManager.equals(mText)) { if (android.view.autofill.Helper.sVerbose) { - Log.v(LOG_TAG, "sendAfterTextChanged(): notify AFM for text=" + mText); + Log.v(LOG_TAG, "notifying AFM after text changed"); } afm.notifyValueChanged(TextView.this); + mLastValueSentToAutofillManager = mText; + } else { + if (android.view.autofill.Helper.sVerbose) { + Log.v(LOG_TAG, "not notifying AFM on unchanged text"); + } } } @@ -9893,9 +10021,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (mEditor != null) mEditor.onFocusChanged(focused, direction); if (focused) { - if (mText instanceof Spannable) { - Spannable sp = (Spannable) mText; - MetaKeyKeyListener.resetMetaState(sp); + if (mSpannable != null) { + MetaKeyKeyListener.resetMetaState(mSpannable); } } @@ -9933,7 +10060,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener */ public void clearComposingText() { if (mText instanceof Spannable) { - BaseInputConnection.removeComposingSpans((Spannable) mText); + BaseInputConnection.removeComposingSpans(mSpannable); } } @@ -9989,7 +10116,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener boolean handled = false; if (mMovement != null) { - handled |= mMovement.onTouchEvent(this, (Spannable) mText, event); + handled |= mMovement.onTouchEvent(this, mSpannable, event); } final boolean textIsSelectable = isTextSelectable(); @@ -9997,7 +10124,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener // The LinkMovementMethod which should handle taps on links has not been installed // on non editable text that support text selection. // We reproduce its behavior here to open links for these. - ClickableSpan[] links = ((Spannable) mText).getSpans(getSelectionStart(), + ClickableSpan[] links = mSpannable.getSpans(getSelectionStart(), getSelectionEnd(), ClickableSpan.class); if (links.length > 0) { @@ -10032,7 +10159,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener public boolean onGenericMotionEvent(MotionEvent event) { if (mMovement != null && mText instanceof Spannable && mLayout != null) { try { - if (mMovement.onGenericMotionEvent(this, (Spannable) mText, event)) { + if (mMovement.onGenericMotionEvent(this, mSpannable, event)) { return true; } } catch (AbstractMethodError ex) { @@ -10093,8 +10220,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener @Override public boolean onTrackballEvent(MotionEvent event) { - if (mMovement != null && mText instanceof Spannable && mLayout != null) { - if (mMovement.onTrackballEvent(this, (Spannable) mText, event)) { + if (mMovement != null && mSpannable != null && mLayout != null) { + if (mMovement.onTrackballEvent(this, mSpannable, event)) { return true; } } @@ -10833,7 +10960,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener final boolean ltrLine = mLayout.getParagraphDirection(line) == Layout.DIR_LEFT_TO_RIGHT; final float[] widths = new float[offsetEnd - offsetStart]; - mLayout.getPaint().getTextWidths(mText, offsetStart, offsetEnd, widths); + mLayout.getPaint().getTextWidths(mTransformed, offsetStart, offsetEnd, widths); final float top = mLayout.getLineTop(line); final float bottom = mLayout.getLineBottom(line); for (int offset = offsetStart; offset < offsetEnd; ++offset) { @@ -11015,7 +11142,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (mText != null) { int updatedTextLength = mText.length(); if (updatedTextLength > 0) { - Selection.setSelection((Spannable) mText, updatedTextLength); + Selection.setSelection(mSpannable, updatedTextLength); } } } return true; @@ -11403,29 +11530,132 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener @NonNull public TextClassifier getTextClassifier() { if (mTextClassifier == null) { - TextClassificationManager tcm = + final TextClassificationManager tcm = mContext.getSystemService(TextClassificationManager.class); if (tcm != null) { - mTextClassifier = tcm.getTextClassifier(); - } else { - mTextClassifier = TextClassifier.NO_OP; + return tcm.getTextClassifier(); } + return TextClassifier.NO_OP; } return mTextClassifier; } /** - * Starts an ActionMode for the specified TextLink. + * Returns a session-aware text classifier. + * This method creates one if none already exists or the current one is destroyed. + */ + @NonNull + TextClassifier getTextClassificationSession() { + if (mTextClassificationSession == null || mTextClassificationSession.isDestroyed()) { + final TextClassificationManager tcm = + mContext.getSystemService(TextClassificationManager.class); + if (tcm != null) { + final String widgetType; + if (isTextEditable()) { + widgetType = TextClassifier.WIDGET_TYPE_EDITTEXT; + } else if (isTextSelectable()) { + widgetType = TextClassifier.WIDGET_TYPE_TEXTVIEW; + } else { + widgetType = TextClassifier.WIDGET_TYPE_UNSELECTABLE_TEXTVIEW; + } + // TODO: Tagged this widgetType with a * so it we can monitor if it reports + // SelectionEvents exactly as the older Logger does. Remove once investigations + // are complete. + final TextClassificationContext textClassificationContext = + new TextClassificationContext.Builder( + mContext.getPackageName(), "*" + widgetType) + .build(); + if (mTextClassifier != null) { + mTextClassificationSession = tcm.createTextClassificationSession( + textClassificationContext, mTextClassifier); + } else { + mTextClassificationSession = tcm.createTextClassificationSession( + textClassificationContext); + } + } else { + mTextClassificationSession = TextClassifier.NO_OP; + } + } + return mTextClassificationSession; + } + + /** + * Returns true if this TextView uses a no-op TextClassifier. + */ + boolean usesNoOpTextClassifier() { + return getTextClassifier() == TextClassifier.NO_OP; + } + + + /** + * Starts an ActionMode for the specified TextLinkSpan. * * @return Whether or not we're attempting to start the action mode. * @hide */ - public boolean requestActionMode(@NonNull TextLinks.TextLink link) { - Preconditions.checkNotNull(link); + public boolean requestActionMode(@NonNull TextLinks.TextLinkSpan clickedSpan) { + Preconditions.checkNotNull(clickedSpan); + + if (!(mText instanceof Spanned)) { + return false; + } + + final int start = ((Spanned) mText).getSpanStart(clickedSpan); + final int end = ((Spanned) mText).getSpanEnd(clickedSpan); + + if (start < 0 || end > mText.length() || start >= end) { + return false; + } + createEditorIfNeeded(); - mEditor.startLinkActionModeAsync(link); + mEditor.startLinkActionModeAsync(start, end); return true; } + + /** + * Handles a click on the specified TextLinkSpan. + * + * @return Whether or not the click is being handled. + * @hide + */ + public boolean handleClick(@NonNull TextLinks.TextLinkSpan clickedSpan) { + Preconditions.checkNotNull(clickedSpan); + if (mText instanceof Spanned) { + final Spanned spanned = (Spanned) mText; + final int start = spanned.getSpanStart(clickedSpan); + final int end = spanned.getSpanEnd(clickedSpan); + if (start >= 0 && end <= mText.length() && start < end) { + final TextClassification.Request request = new TextClassification.Request.Builder( + mText, start, end) + .setDefaultLocales(getTextLocales()) + .build(); + final Supplier<TextClassification> supplier = () -> + getTextClassifier().classifyText(request); + final Consumer<TextClassification> consumer = classification -> { + if (classification != null) { + if (!classification.getActions().isEmpty()) { + try { + classification.getActions().get(0).getActionIntent().send(); + } catch (PendingIntent.CanceledException e) { + Log.e(LOG_TAG, "Error sending PendingIntent", e); + } + } else { + Log.d(LOG_TAG, "No link action to perform"); + } + } else { + // classification == null + Log.d(LOG_TAG, "Timeout while classifying text"); + } + }; + CompletableFuture.supplyAsync(supplier) + .completeOnTimeout(null, 1, TimeUnit.SECONDS) + .thenAccept(consumer); + return true; + } + } + return false; + } + /** * @hide */ @@ -11435,6 +11665,13 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } + /** @hide */ + public void hideFloatingToolbar(int durationMs) { + if (mEditor != null) { + mEditor.hideFloatingToolbar(durationMs); + } + } + boolean canUndo() { return mEditor != null && mEditor.canUndo(); } @@ -11529,10 +11766,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener boolean selectAllText() { if (mEditor != null) { // Hide the toolbar before changing the selection to avoid flickering. - mEditor.hideFloatingToolbar(FLOATING_TOOLBAR_SELECT_ALL_REFRESH_DELAY); + hideFloatingToolbar(FLOATING_TOOLBAR_SELECT_ALL_REFRESH_DELAY); } final int length = mText.length(); - Selection.setSelection((Spannable) mText, 0, length); + Selection.setSelection(mSpannable, 0, length); return length > 0; } @@ -11560,7 +11797,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } if (paste != null) { if (!didFirst) { - Selection.setSelection((Spannable) mText, max); + Selection.setSelection(mSpannable, max); ((Editable) mText).replace(min, max, paste); didFirst = true; } else { @@ -11582,7 +11819,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener selectedText = TextUtils.trimToParcelableSize(selectedText); sharingIntent.putExtra(android.content.Intent.EXTRA_TEXT, selectedText); getContext().startActivity(Intent.createChooser(sharingIntent, null)); - Selection.setSelection((Spannable) mText, getSelectionEnd()); + Selection.setSelection(mSpannable, getSelectionEnd()); } } @@ -11657,7 +11894,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener case DragEvent.ACTION_DRAG_LOCATION: if (mText instanceof Spannable) { final int offset = getOffsetForPosition(event.getX(), event.getY()); - Selection.setSelection((Spannable) mText, offset); + Selection.setSelection(mSpannable, offset); } return true; @@ -11695,6 +11932,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } /** + * Returns the current {@link TextDirectionHeuristic}. + * + * @return the current {@link TextDirectionHeuristic}. * @hide */ protected TextDirectionHeuristic getTextDirectionHeuristic() { @@ -12288,9 +12528,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener + " before=" + before + " after=" + after + ": " + buffer); } - if (AccessibilityManager.getInstance(mContext).isEnabled() - && !isPasswordInputType(getInputType()) && !hasPasswordTransformationMethod()) { - mBeforeText = buffer.toString(); + if (AccessibilityManager.getInstance(mContext).isEnabled() && (mTransformed != null)) { + mBeforeText = mTransformed.toString(); } TextView.this.sendBeforeTextChanged(buffer, start, before, after); diff --git a/android/widget/TextViewPrecomputedTextPerfTest.java b/android/widget/TextViewPrecomputedTextPerfTest.java new file mode 100644 index 00000000..dc34b7fe --- /dev/null +++ b/android/widget/TextViewPrecomputedTextPerfTest.java @@ -0,0 +1,455 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package android.widget; + +import static android.view.View.MeasureSpec.AT_MOST; +import static android.view.View.MeasureSpec.EXACTLY; +import static android.view.View.MeasureSpec.UNSPECIFIED; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.graphics.Typeface; +import android.graphics.Canvas; +import android.perftests.utils.BenchmarkState; +import android.perftests.utils.PerfStatusReporter; +import android.support.test.InstrumentationRegistry; +import android.support.test.filters.LargeTest; +import android.support.test.runner.AndroidJUnit4; +import android.text.PrecomputedText; +import android.text.Layout; +import android.text.BoringLayout; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.TextPaint; +import android.text.style.TextAppearanceSpan; +import android.view.LayoutInflater; +import android.text.TextPerfUtils; +import android.view.View.MeasureSpec; +import android.view.DisplayListCanvas; +import android.view.RenderNode; + +import com.android.perftests.core.R; + +import java.util.Random; +import java.util.Locale; + +import org.junit.Before; +import org.junit.Test; +import org.junit.Rule; +import org.junit.runner.RunWith; + +import static org.junit.Assert.assertTrue; + +import static android.widget.TextView.UNKNOWN_BORING; + +@LargeTest +@RunWith(AndroidJUnit4.class) +public class TextViewPrecomputedTextPerfTest { + private static final int WORD_LENGTH = 9; // Random word has 9 characters. + private static final int WORDS_IN_LINE = 8; // Roughly, 8 words in a line. + private static final boolean NO_STYLE_TEXT = false; + private static final boolean STYLE_TEXT = true; + + private static TextPaint PAINT = new TextPaint(); + private static final int TEXT_WIDTH = WORDS_IN_LINE * WORD_LENGTH * (int) PAINT.getTextSize(); + + public TextViewPrecomputedTextPerfTest() {} + + private static class TestableTextView extends TextView { + public TestableTextView(Context ctx) { + super(ctx); + } + + public void onMeasure(int w, int h) { + super.onMeasure(w, h); + } + + public void onDraw(Canvas canvas) { + super.onDraw(canvas); + } + } + + @Rule + public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); + + private TextPerfUtils mTextUtil = new TextPerfUtils(); + + @Before + public void setUp() { + mTextUtil.resetRandom(0 /* seed */); + } + + private static Context getContext() { + return InstrumentationRegistry.getTargetContext(); + } + + @Test + public void testNewLayout_RandomText() { + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + BoringLayout.Metrics metrics = new BoringLayout.Metrics(); + while (state.keepRunning()) { + state.pauseTiming(); + final CharSequence text = mTextUtil.nextRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT); + final TextView textView = new TextView(getContext()); + textView.setBreakStrategy(Layout.BREAK_STRATEGY_BALANCED); + textView.setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL); + textView.setText(text); + Canvas.freeTextLayoutCaches(); + state.resumeTiming(); + + textView.makeNewLayout(TEXT_WIDTH, TEXT_WIDTH, UNKNOWN_BORING, UNKNOWN_BORING, + TEXT_WIDTH, false); + } + } + + @Test + public void testNewLayout_RandomText_Selectable() { + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + BoringLayout.Metrics metrics = new BoringLayout.Metrics(); + while (state.keepRunning()) { + state.pauseTiming(); + final CharSequence text = mTextUtil.nextRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT); + final TextView textView = new TextView(getContext()); + textView.setTextIsSelectable(true); + textView.setBreakStrategy(Layout.BREAK_STRATEGY_BALANCED); + textView.setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL); + textView.setText(text); + Canvas.freeTextLayoutCaches(); + state.resumeTiming(); + + textView.makeNewLayout(TEXT_WIDTH, TEXT_WIDTH, UNKNOWN_BORING, UNKNOWN_BORING, + TEXT_WIDTH, false); + } + } + + @Test + public void testNewLayout_PrecomputedText() { + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + BoringLayout.Metrics metrics = new BoringLayout.Metrics(); + while (state.keepRunning()) { + state.pauseTiming(); + final PrecomputedText.Params params = new PrecomputedText.Params.Builder(PAINT) + .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL) + .setBreakStrategy(Layout.BREAK_STRATEGY_BALANCED).build(); + final CharSequence text = PrecomputedText.create( + mTextUtil.nextRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), params); + final TextView textView = new TextView(getContext()); + textView.setTextMetricsParams(params); + textView.setText(text); + Canvas.freeTextLayoutCaches(); + state.resumeTiming(); + + textView.makeNewLayout(TEXT_WIDTH, TEXT_WIDTH, UNKNOWN_BORING, UNKNOWN_BORING, + TEXT_WIDTH, false); + } + } + + @Test + public void testNewLayout_PrecomputedText_Selectable() { + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + BoringLayout.Metrics metrics = new BoringLayout.Metrics(); + while (state.keepRunning()) { + state.pauseTiming(); + final PrecomputedText.Params params = new PrecomputedText.Params.Builder(PAINT) + .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL) + .setBreakStrategy(Layout.BREAK_STRATEGY_BALANCED).build(); + final CharSequence text = PrecomputedText.create( + mTextUtil.nextRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), params); + final TextView textView = new TextView(getContext()); + textView.setTextIsSelectable(true); + textView.setTextMetricsParams(params); + textView.setText(text); + Canvas.freeTextLayoutCaches(); + state.resumeTiming(); + + textView.makeNewLayout(TEXT_WIDTH, TEXT_WIDTH, UNKNOWN_BORING, UNKNOWN_BORING, + TEXT_WIDTH, false); + } + } + + @Test + public void testSetText_RandomText() { + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + BoringLayout.Metrics metrics = new BoringLayout.Metrics(); + while (state.keepRunning()) { + state.pauseTiming(); + final CharSequence text = mTextUtil.nextRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT); + final TextView textView = new TextView(getContext()); + textView.setBreakStrategy(Layout.BREAK_STRATEGY_BALANCED); + textView.setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL); + Canvas.freeTextLayoutCaches(); + state.resumeTiming(); + + textView.setText(text); + } + } + + @Test + public void testSetText_RandomText_Selectable() { + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + BoringLayout.Metrics metrics = new BoringLayout.Metrics(); + while (state.keepRunning()) { + state.pauseTiming(); + final CharSequence text = mTextUtil.nextRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT); + final TextView textView = new TextView(getContext()); + textView.setTextIsSelectable(true); + textView.setBreakStrategy(Layout.BREAK_STRATEGY_BALANCED); + textView.setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL); + Canvas.freeTextLayoutCaches(); + state.resumeTiming(); + + textView.setText(text); + } + } + + @Test + public void testSetText_PrecomputedText() { + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + BoringLayout.Metrics metrics = new BoringLayout.Metrics(); + while (state.keepRunning()) { + state.pauseTiming(); + final PrecomputedText.Params params = new PrecomputedText.Params.Builder(PAINT) + .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL) + .setBreakStrategy(Layout.BREAK_STRATEGY_BALANCED).build(); + final CharSequence text = PrecomputedText.create( + mTextUtil.nextRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), params); + final TextView textView = new TextView(getContext()); + textView.setTextMetricsParams(params); + Canvas.freeTextLayoutCaches(); + state.resumeTiming(); + + textView.setText(text); + } + } + + @Test + public void testSetText_PrecomputedText_Selectable() { + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + BoringLayout.Metrics metrics = new BoringLayout.Metrics(); + while (state.keepRunning()) { + state.pauseTiming(); + final PrecomputedText.Params params = new PrecomputedText.Params.Builder(PAINT) + .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL) + .setBreakStrategy(Layout.BREAK_STRATEGY_BALANCED).build(); + final CharSequence text = PrecomputedText.create( + mTextUtil.nextRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), params); + final TextView textView = new TextView(getContext()); + textView.setTextIsSelectable(true); + textView.setTextMetricsParams(params); + Canvas.freeTextLayoutCaches(); + state.resumeTiming(); + + textView.setText(text); + } + } + + @Test + public void testOnMeasure_RandomText() { + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + int width = MeasureSpec.makeMeasureSpec(TEXT_WIDTH, MeasureSpec.AT_MOST); + int height = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + while (state.keepRunning()) { + state.pauseTiming(); + final CharSequence text = mTextUtil.nextRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT); + final TestableTextView textView = new TestableTextView(getContext()); + textView.setBreakStrategy(Layout.BREAK_STRATEGY_BALANCED); + textView.setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL); + textView.setText(text); + textView.nullLayouts(); + Canvas.freeTextLayoutCaches(); + state.resumeTiming(); + + textView.onMeasure(width, height); + } + } + + @Test + public void testOnMeasure_RandomText_Selectable() { + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + int width = MeasureSpec.makeMeasureSpec(TEXT_WIDTH, MeasureSpec.AT_MOST); + int height = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + while (state.keepRunning()) { + state.pauseTiming(); + final CharSequence text = mTextUtil.nextRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT); + final TestableTextView textView = new TestableTextView(getContext()); + textView.setTextIsSelectable(true); + textView.setBreakStrategy(Layout.BREAK_STRATEGY_BALANCED); + textView.setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL); + textView.setText(text); + textView.nullLayouts(); + Canvas.freeTextLayoutCaches(); + state.resumeTiming(); + + textView.onMeasure(width, height); + } + } + + @Test + public void testOnMeasure_PrecomputedText() { + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + int width = MeasureSpec.makeMeasureSpec(TEXT_WIDTH, MeasureSpec.AT_MOST); + int height = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + while (state.keepRunning()) { + state.pauseTiming(); + final PrecomputedText.Params params = new PrecomputedText.Params.Builder(PAINT) + .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL) + .setBreakStrategy(Layout.BREAK_STRATEGY_BALANCED).build(); + final CharSequence text = PrecomputedText.create( + mTextUtil.nextRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), params); + final TestableTextView textView = new TestableTextView(getContext()); + textView.setTextMetricsParams(params); + textView.setText(text); + textView.nullLayouts(); + Canvas.freeTextLayoutCaches(); + state.resumeTiming(); + + textView.onMeasure(width, height); + } + } + + @Test + public void testOnMeasure_PrecomputedText_Selectable() { + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + int width = MeasureSpec.makeMeasureSpec(TEXT_WIDTH, MeasureSpec.AT_MOST); + int height = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + while (state.keepRunning()) { + state.pauseTiming(); + final PrecomputedText.Params params = new PrecomputedText.Params.Builder(PAINT) + .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL) + .setBreakStrategy(Layout.BREAK_STRATEGY_BALANCED).build(); + final CharSequence text = PrecomputedText.create( + mTextUtil.nextRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), params); + final TestableTextView textView = new TestableTextView(getContext()); + textView.setTextIsSelectable(true); + textView.setTextMetricsParams(params); + textView.setText(text); + textView.nullLayouts(); + Canvas.freeTextLayoutCaches(); + state.resumeTiming(); + + textView.onMeasure(width, height); + } + } + + @Test + public void testOnDraw_RandomText() { + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + int width = MeasureSpec.makeMeasureSpec(TEXT_WIDTH, MeasureSpec.AT_MOST); + int height = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + final RenderNode node = RenderNode.create("benchmark", null); + while (state.keepRunning()) { + state.pauseTiming(); + final CharSequence text = mTextUtil.nextRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT); + final TestableTextView textView = new TestableTextView(getContext()); + textView.setBreakStrategy(Layout.BREAK_STRATEGY_BALANCED); + textView.setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL); + textView.setText(text); + textView.measure(width, height); + textView.layout(0, 0, textView.getMeasuredWidth(), textView.getMeasuredHeight()); + final DisplayListCanvas c = node.start( + textView.getMeasuredWidth(), textView.getMeasuredHeight()); + textView.nullLayouts(); + Canvas.freeTextLayoutCaches(); + state.resumeTiming(); + + textView.onDraw(c); + } + } + + @Test + public void testOnDraw_RandomText_Selectable() { + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + int width = MeasureSpec.makeMeasureSpec(TEXT_WIDTH, MeasureSpec.AT_MOST); + int height = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + final RenderNode node = RenderNode.create("benchmark", null); + while (state.keepRunning()) { + state.pauseTiming(); + final CharSequence text = mTextUtil.nextRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT); + final TestableTextView textView = new TestableTextView(getContext()); + textView.setTextIsSelectable(true); + textView.setBreakStrategy(Layout.BREAK_STRATEGY_BALANCED); + textView.setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL); + textView.setText(text); + textView.measure(width, height); + textView.layout(0, 0, textView.getMeasuredWidth(), textView.getMeasuredHeight()); + final DisplayListCanvas c = node.start( + textView.getMeasuredWidth(), textView.getMeasuredHeight()); + textView.nullLayouts(); + Canvas.freeTextLayoutCaches(); + state.resumeTiming(); + + textView.onDraw(c); + } + } + + @Test + public void testOnDraw_PrecomputedText() { + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + int width = MeasureSpec.makeMeasureSpec(TEXT_WIDTH, MeasureSpec.AT_MOST); + int height = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + final RenderNode node = RenderNode.create("benchmark", null); + while (state.keepRunning()) { + state.pauseTiming(); + final PrecomputedText.Params params = new PrecomputedText.Params.Builder(PAINT) + .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL) + .setBreakStrategy(Layout.BREAK_STRATEGY_BALANCED).build(); + final CharSequence text = PrecomputedText.create( + mTextUtil.nextRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), params); + final TestableTextView textView = new TestableTextView(getContext()); + textView.setTextMetricsParams(params); + textView.setText(text); + textView.measure(width, height); + textView.layout(0, 0, textView.getMeasuredWidth(), textView.getMeasuredHeight()); + final DisplayListCanvas c = node.start( + textView.getMeasuredWidth(), textView.getMeasuredHeight()); + textView.nullLayouts(); + Canvas.freeTextLayoutCaches(); + state.resumeTiming(); + + textView.onDraw(c); + } + } + + @Test + public void testOnDraw_PrecomputedText_Selectable() { + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + int width = MeasureSpec.makeMeasureSpec(MeasureSpec.AT_MOST, TEXT_WIDTH); + int height = MeasureSpec.makeMeasureSpec(MeasureSpec.UNSPECIFIED, 0); + final RenderNode node = RenderNode.create("benchmark", null); + while (state.keepRunning()) { + state.pauseTiming(); + final PrecomputedText.Params params = new PrecomputedText.Params.Builder(PAINT) + .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL) + .setBreakStrategy(Layout.BREAK_STRATEGY_BALANCED).build(); + final CharSequence text = PrecomputedText.create( + mTextUtil.nextRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), params); + final TestableTextView textView = new TestableTextView(getContext()); + textView.setTextIsSelectable(true); + textView.setTextMetricsParams(params); + textView.setText(text); + textView.measure(width, height); + textView.layout(0, 0, textView.getMeasuredWidth(), textView.getMeasuredHeight()); + final DisplayListCanvas c = node.start( + textView.getMeasuredWidth(), textView.getMeasuredHeight()); + textView.nullLayouts(); + Canvas.freeTextLayoutCaches(); + state.resumeTiming(); + + textView.onDraw(c); + } + } +} diff --git a/android/widget/TimePickerClockDelegate.java b/android/widget/TimePickerClockDelegate.java index 706b0ce2..77670b35 100644 --- a/android/widget/TimePickerClockDelegate.java +++ b/android/widget/TimePickerClockDelegate.java @@ -50,6 +50,7 @@ import com.android.internal.R; import com.android.internal.widget.NumericTextView; import com.android.internal.widget.NumericTextView.OnValueChangedListener; + import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Calendar; @@ -804,20 +805,56 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate { private void updateHeaderSeparator() { final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mLocale, (mIs24Hour) ? "Hm" : "hm"); - final String separatorText; - // See http://www.unicode.org/reports/tr35/tr35-dates.html for hour formats - final char[] hourFormats = {'H', 'h', 'K', 'k'}; - int hIndex = lastIndexOfAny(bestDateTimePattern, hourFormats); - if (hIndex == -1) { - // Default case - separatorText = ":"; - } else { - separatorText = Character.toString(bestDateTimePattern.charAt(hIndex + 1)); - } + final String separatorText = getHourMinSeparatorFromPattern(bestDateTimePattern); mSeparatorView.setText(separatorText); mTextInputPickerView.updateSeparator(separatorText); } + /** + * This helper method extracts the time separator from the {@code datetimePattern}. + * + * The time separator is defined in the Unicode CLDR and cannot be supposed to be ":". + * + * See http://unicode.org/cldr/trac/browser/trunk/common/main + * + * @return Separator string. This is the character or set of quoted characters just after the + * hour marker in {@code dateTimePattern}. Returns a colon (:) if it can't locate the + * separator. + * + * @hide + */ + private static String getHourMinSeparatorFromPattern(String dateTimePattern) { + final String defaultSeparator = ":"; + boolean foundHourPattern = false; + for (int i = 0; i < dateTimePattern.length(); i++) { + switch (dateTimePattern.charAt(i)) { + // See http://www.unicode.org/reports/tr35/tr35-dates.html for hour formats. + case 'H': + case 'h': + case 'K': + case 'k': + foundHourPattern = true; + continue; + case ' ': // skip spaces + continue; + case '\'': + if (!foundHourPattern) { + continue; + } + SpannableStringBuilder quotedSubstring = new SpannableStringBuilder( + dateTimePattern.substring(i)); + int quotedTextLength = DateFormat.appendQuotedText(quotedSubstring, 0); + return quotedSubstring.subSequence(0, quotedTextLength).toString(); + default: + if (!foundHourPattern) { + continue; + } + return Character.toString(dateTimePattern.charAt(i)); + } + } + return defaultSeparator; + } + static private int lastIndexOfAny(String str, char[] any) { final int lengthAny = any.length; if (lengthAny > 0) { diff --git a/android/widget/Toast.java b/android/widget/Toast.java index edcf209b..d74a60e4 100644 --- a/android/widget/Toast.java +++ b/android/widget/Toast.java @@ -531,6 +531,14 @@ public class Toast { mWM.removeViewImmediate(mView); } + + // Now that we've removed the view it's safe for the server to release + // the resources. + try { + getService().finishToken(mPackageName, this); + } catch (RemoteException e) { + } + mView = null; } } diff --git a/android/widget/VideoView2.java b/android/widget/VideoView2.java index 8650c0a0..388eae2a 100644 --- a/android/widget/VideoView2.java +++ b/android/widget/VideoView2.java @@ -22,32 +22,40 @@ import android.annotation.Nullable; import android.content.Context; import android.media.AudioAttributes; import android.media.AudioManager; -import android.media.MediaPlayerBase; +import android.media.DataSourceDesc; +import android.media.MediaItem2; +import android.media.MediaMetadata2; +import android.media.MediaPlayer2; +import android.media.SessionToken2; +import android.media.session.MediaController; +import android.media.session.PlaybackState; import android.media.update.ApiLoader; import android.media.update.VideoView2Provider; -import android.media.update.ViewProvider; +import android.media.update.ViewGroupHelper; import android.net.Uri; +import android.os.Bundle; import android.util.AttributeSet; -import android.view.KeyEvent; -import android.view.MotionEvent; +import android.view.View; + +import com.android.internal.annotations.VisibleForTesting; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.List; import java.util.Map; +import java.util.concurrent.Executor; -// TODO: Use @link tag to refer MediaPlayer2 in docs once MediaPlayer2.java is submitted. Same to -// MediaSession2. -// TODO: change the reference from MediaPlayer to MediaPlayer2. +// TODO: Replace MediaSession wtih MediaSession2 once MediaSession2 is submitted. /** - * Displays a video file. VideoView2 class is a View class which is wrapping MediaPlayer2 so that - * developers can easily implement a video rendering application. + * @hide + * Displays a video file. VideoView2 class is a View class which is wrapping {@link MediaPlayer2} + * so that developers can easily implement a video rendering application. * * <p> * <em> Data sources that VideoView2 supports : </em> - * VideoView2 can play video files and audio-only fiels as + * VideoView2 can play video files and audio-only files as * well. It can load from various sources such as resources or content providers. The supported - * media file formats are the same as MediaPlayer2. + * media file formats are the same as {@link MediaPlayer2}. * * <p> * <em> View type can be selected : </em> @@ -76,8 +84,8 @@ import java.util.Map; * If a developer wants to attach a customed MediaControlView2, then set enableControlView attribute * to false and assign the customed media control widget using {@link #setMediaControlView2}. * <li> VideoView2 is integrated with MediaPlayer2 while VideoView is integrated with MediaPlayer. - * <li> VideoView2 is integrated with MediaSession2 and so it responses with media key events. - * A VideoView2 keeps a MediaSession2 instance internally and connects it to a corresponding + * <li> VideoView2 is integrated with MediaSession and so it responses with media key events. + * A VideoView2 keeps a MediaSession instance internally and connects it to a corresponding * MediaControlView2 instance. * </p> * </ul> @@ -96,10 +104,8 @@ import java.util.Map; * does not restore the current play state, play position, selected tracks. Applications should save * and restore these on their own in {@link android.app.Activity#onSaveInstanceState} and * {@link android.app.Activity#onRestoreInstanceState}. - * - * @hide */ -public class VideoView2 extends FrameLayout { +public class VideoView2 extends ViewGroupHelper<VideoView2Provider> { /** @hide */ @IntDef({ VIEW_TYPE_TEXTUREVIEW, @@ -108,10 +114,19 @@ public class VideoView2 extends FrameLayout { @Retention(RetentionPolicy.SOURCE) public @interface ViewType {} + /** + * Indicates video is rendering on SurfaceView. + * + * @see #setViewType + */ public static final int VIEW_TYPE_SURFACEVIEW = 1; - public static final int VIEW_TYPE_TEXTUREVIEW = 2; - private final VideoView2Provider mProvider; + /** + * Indicates video is rendering on TextureView. + * + * @see #setViewType + */ + public static final int VIEW_TYPE_TEXTUREVIEW = 2; public VideoView2(@NonNull Context context) { this(context, null); @@ -128,17 +143,12 @@ public class VideoView2 extends FrameLayout { public VideoView2( @NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - - mProvider = ApiLoader.getProvider(context).createVideoView2(this, new SuperProvider(), - attrs, defStyleAttr, defStyleRes); - } - - /** - * @hide - */ - public VideoView2Provider getProvider() { - return mProvider; + super((instance, superProvider, privateProvider) -> + ApiLoader.getProvider().createVideoView2( + (VideoView2) instance, superProvider, privateProvider, + attrs, defStyleAttr, defStyleRes), + context, attrs, defStyleAttr, defStyleRes); + mProvider.initialize(attrs, defStyleAttr, defStyleRes); } /** @@ -146,9 +156,10 @@ public class VideoView2 extends FrameLayout { * instance if any. * * @param mediaControlView a media control view2 instance. + * @param intervalMs a time interval in milliseconds until VideoView2 hides MediaControlView2. */ - public void setMediaControlView2(MediaControlView2 mediaControlView) { - mProvider.setMediaControlView2_impl(mediaControlView); + public void setMediaControlView2(MediaControlView2 mediaControlView, long intervalMs) { + mProvider.setMediaControlView2_impl(mediaControlView, intervalMs); } /** @@ -160,91 +171,71 @@ public class VideoView2 extends FrameLayout { } /** - * Starts playback with the media contents specified by {@link #setVideoURI} and - * {@link #setVideoPath}. - * If it has been paused, this method will resume playback from the current position. - */ - public void start() { - mProvider.start_impl(); - } - - /** - * Pauses playback. - */ - public void pause() { - mProvider.pause_impl(); - } - - /** - * Gets the duration of the media content specified by #setVideoURI and #setVideoPath - * in milliseconds. - */ - public int getDuration() { - return mProvider.getDuration_impl(); - } - - /** - * Gets current playback position in milliseconds. - */ - public int getCurrentPosition() { - return mProvider.getCurrentPosition_impl(); - } - - // TODO: mention about key-frame related behavior. - /** - * Moves the media by specified time position. - * @param msec the offset in milliseconds from the start to seek to. - */ - public void seekTo(int msec) { - mProvider.seekTo_impl(msec); - } - - /** - * Says if the media is currently playing. - * @return true if the media is playing, false if it is not (eg. paused or stopped). + * Sets MediaMetadata2 instance. It will replace the previously assigned MediaMetadata2 instance + * if any. + * + * @param metadata a MediaMetadata2 instance. + * @hide */ - public boolean isPlaying() { - return mProvider.isPlaying_impl(); + public void setMediaMetadata(MediaMetadata2 metadata) { + mProvider.setMediaMetadata_impl(metadata); } - // TODO: check what will return if it is a local media. /** - * Gets the percentage (0-100) of the content that has been buffered or played so far. + * Returns MediaMetadata2 instance which is retrieved from MediaPlayer2 inside VideoView2 by + * default or by {@link #setMediaMetadata} method. + * @hide */ - public int getBufferPercentage() { - return mProvider.getBufferPercentage_impl(); + public MediaMetadata2 getMediaMetadata() { + // TODO: add to Javadoc whether this value can be null or not when integrating with + // MediaSession2. + return mProvider.getMediaMetadata_impl(); } /** - * Returns the audio session ID. + * Returns MediaController instance which is connected with MediaSession that VideoView2 is + * using. This method should be called when VideoView2 is attached to window, or it throws + * IllegalStateException, since internal MediaSession instance is not available until + * this view is attached to window. Please check {@link android.view.View#isAttachedToWindow} + * before calling this method. + * + * @throws IllegalStateException if interal MediaSession is not created yet. + * @hide TODO: remove */ - public int getAudioSessionId() { - return mProvider.getAudioSessionId_impl(); + public MediaController getMediaController() { + return mProvider.getMediaController_impl(); } /** - * Starts rendering closed caption or subtitles if there is any. The first subtitle track will - * be chosen by default if there multiple subtitle tracks exist. + * Returns {@link android.media.SessionToken2} so that developers create their own + * {@link android.media.MediaController2} instance. This method should be called when VideoView2 + * is attached to window, or it throws IllegalStateException. + * + * @throws IllegalStateException if interal MediaSession is not created yet. */ - public void showSubtitle() { - mProvider.showSubtitle_impl(); + public SessionToken2 getMediaSessionToken() { + return mProvider.getMediaSessionToken_impl(); } /** - * Stops showing closed captions or subtitles. + * Shows or hides closed caption or subtitles if there is any. + * The first subtitle track will be chosen if there multiple subtitle tracks exist. + * Default behavior of VideoView2 is not showing subtitle. + * @param enable shows closed caption or subtitles if this value is true, or hides. */ - public void hideSubtitle() { - mProvider.hideSubtitle_impl(); + public void setSubtitleEnabled(boolean enable) { + mProvider.setSubtitleEnabled_impl(enable); } /** - * Sets full screen mode. + * Returns true if showing subtitle feature is enabled or returns false. + * Although there is no subtitle track or closed caption, it can return true, if the feature + * has been enabled by {@link #setSubtitleEnabled}. */ - public void setFullScreen(boolean fullScreen) { - mProvider.setFullScreen_impl(fullScreen); + public boolean isSubtitleEnabled() { + return mProvider.isSubtitleEnabled_impl(); } - // TODO: This should be revised after integration with MediaPlayer2. /** * Sets playback speed. * @@ -254,21 +245,12 @@ public class VideoView2 extends FrameLayout { * be reset to the normal speed 1.0f. * @param speed the playback speed. It should be positive. */ + // TODO: Support this via MediaController2. public void setSpeed(float speed) { mProvider.setSpeed_impl(speed); } /** - * Returns current speed setting. - * - * If setSpeed() has never been called, returns the default value 1.0f. - * @return current speed setting - */ - public float getSpeed() { - return mProvider.getSpeed_impl(); - } - - /** * Sets which type of audio focus will be requested during the playback, or configures playback * to not request audio focus. Valid values for focus requests are * {@link AudioManager#AUDIOFOCUS_GAIN}, {@link AudioManager#AUDIOFOCUS_GAIN_TRANSIENT}, @@ -297,24 +279,11 @@ public class VideoView2 extends FrameLayout { } /** - * Sets a remote player for handling playback of the selected route from MediaControlView2. - * If this is not called, MediaCotrolView2 will not show the route button. - * - * @param routeCategories the list of media control categories in - * {@link android.support.v7.media.MediaControlIntent} - * @param player the player to handle the selected route. If null, a default - * route player will be used. - * @throws IllegalStateException if MediaControlView2 is not set. - */ - public void setRouteAttributes(@NonNull List<String> routeCategories, - @Nullable MediaPlayerBase player) { - mProvider.setRouteAttributes_impl(routeCategories, player); - } - - /** * Sets video path. * * @param path the path of the video. + * + * @hide TODO remove */ public void setVideoPath(String path) { mProvider.setVideoPath_impl(path); @@ -324,9 +293,11 @@ public class VideoView2 extends FrameLayout { * Sets video URI. * * @param uri the URI of the video. + * + * @hide TODO remove */ - public void setVideoURI(Uri uri) { - mProvider.setVideoURI_impl(uri); + public void setVideoUri(Uri uri) { + mProvider.setVideoUri_impl(uri); } /** @@ -338,9 +309,30 @@ public class VideoView2 extends FrameLayout { * changed with key/value pairs through the headers parameter with * "android-allow-cross-domain-redirect" as the key and "0" or "1" as the value * to disallow or allow cross domain redirection. + * + * @hide TODO remove */ - public void setVideoURI(Uri uri, Map<String, String> headers) { - mProvider.setVideoURI_impl(uri, headers); + public void setVideoUri(Uri uri, Map<String, String> headers) { + mProvider.setVideoUri_impl(uri, headers); + } + + /** + * Sets {@link MediaItem2} object to render using VideoView2. Alternative way to set media + * object to VideoView2 is {@link #setDataSource}. + * @param mediaItem the MediaItem2 to play + * @see #setDataSource + */ + public void setMediaItem(@NonNull MediaItem2 mediaItem) { + mProvider.setMediaItem_impl(mediaItem); + } + + /** + * Sets {@link DataSourceDesc} object to render using VideoView2. + * @param dataSource the {@link DataSourceDesc} object to play. + * @see #setMediaItem + */ + public void setDataSource(@NonNull DataSourceDesc dataSource) { + mProvider.setDataSource_impl(dataSource); } /** @@ -367,236 +359,89 @@ public class VideoView2 extends FrameLayout { } /** - * Stops playback and release all the resources. This should be called whenever a VideoView2 - * instance is no longer to be used. - */ - public void stopPlayback() { - mProvider.stopPlayback_impl(); - } - - /** - * Registers a callback to be invoked when the media file is loaded and ready to go. + * Sets custom actions which will be shown as custom buttons in {@link MediaControlView2}. * - * @param l the callback that will be run. + * @param actionList A list of {@link PlaybackState.CustomAction}. The return value of + * {@link PlaybackState.CustomAction#getIcon()} will be used to draw buttons + * in {@link MediaControlView2}. + * @param executor executor to run callbacks on. + * @param listener A listener to be called when a custom button is clicked. + * @hide TODO remove */ - public void setOnPreparedListener(OnPreparedListener l) { - mProvider.setOnPreparedListener_impl(l); - } - - /** - * Registers a callback to be invoked when the end of a media file has been reached during - * playback. - * - * @param l the callback that will be run. - */ - public void setOnCompletionListener(OnCompletionListener l) { - mProvider.setOnCompletionListener_impl(l); - } - - /** - * Registers a callback to be invoked when an error occurs during playback or setup. If no - * listener is specified, or if the listener returned false, VideoView2 will inform the user of - * any errors. - * - * @param l The callback that will be run - */ - public void setOnErrorListener(OnErrorListener l) { - mProvider.setOnErrorListener_impl(l); - } - - /** - * Registers a callback to be invoked when an informational event occurs during playback or - * setup. - * - * @param l The callback that will be run - */ - public void setOnInfoListener(OnInfoListener l) { - mProvider.setOnInfoListener_impl(l); + public void setCustomActions(List<PlaybackState.CustomAction> actionList, + Executor executor, OnCustomActionListener listener) { + mProvider.setCustomActions_impl(actionList, executor, listener); } /** * Registers a callback to be invoked when a view type change is done. * {@see #setViewType(int)} * @param l The callback that will be run + * @hide */ + @VisibleForTesting public void setOnViewTypeChangedListener(OnViewTypeChangedListener l) { mProvider.setOnViewTypeChangedListener_impl(l); } /** * Registers a callback to be invoked when the fullscreen mode should be changed. + * @param l The callback that will be run + * @hide TODO remove */ - public void setFullScreenChangedListener(OnFullScreenChangedListener l) { - mProvider.setFullScreenChangedListener_impl(l); + public void setFullScreenRequestListener(OnFullScreenRequestListener l) { + mProvider.setFullScreenRequestListener_impl(l); } /** - * Interface definition of a callback to be invoked when the viw type has been changed. + * Interface definition of a callback to be invoked when the view type has been changed. + * + * @hide */ + @VisibleForTesting public interface OnViewTypeChangedListener { /** * Called when the view type has been changed. * @see #setViewType(int) + * @param view the View whose view type is changed * @param viewType * <ul> * <li>{@link #VIEW_TYPE_SURFACEVIEW} * <li>{@link #VIEW_TYPE_TEXTUREVIEW} * </ul> */ - void onViewTypeChanged(@ViewType int viewType); - } - - /** - * Interface definition of a callback to be invoked when the media source is ready for playback. - */ - public interface OnPreparedListener { - /** - * Called when the media file is ready for playback. - */ - void onPrepared(); + void onViewTypeChanged(View view, @ViewType int viewType); } /** - * Interface definition for a callback to be invoked when playback of a media source has - * completed. - */ - public interface OnCompletionListener { - /** - * Called when the end of a media source is reached during playback. - */ - void onCompletion(); - } - - /** - * Interface definition of a callback to be invoked when there has been an error during an - * asynchronous operation. + * Interface definition of a callback to be invoked to inform the fullscreen mode is changed. + * Application should handle the fullscreen mode accordingly. + * @hide TODO remove */ - public interface OnErrorListener { - // TODO: Redefine error codes. + public interface OnFullScreenRequestListener { /** - * Called to indicate an error. - * @param what the type of error that has occurred - * @param extra an extra code, specific to the error. - * @return true if the method handled the error, false if it didn't. - * @see MediaPlayer#OnErrorListener + * Called to indicate a fullscreen mode change. */ - boolean onError(int what, int extra); + void onFullScreenRequest(View view, boolean fullScreen); } /** - * Interface definition of a callback to be invoked to communicate some info and/or warning - * about the media or its playback. + * Interface definition of a callback to be invoked to inform that a custom action is performed. + * @hide TODO remove */ - public interface OnInfoListener { + public interface OnCustomActionListener { /** - * Called to indicate an info or a warning. - * @param what the type of info or warning. - * @param extra an extra code, specific to the info. + * Called to indicate that a custom action is performed. * - * @see MediaPlayer#OnInfoListener + * @param action The action that was originally sent in the + * {@link PlaybackState.CustomAction}. + * @param extras Optional extras. */ - void onInfo(int what, int extra); - } - - /** - * Interface definition of a callback to be invoked to inform the fullscreen mode is changed. - */ - public interface OnFullScreenChangedListener { - /** - * Called to indicate a fullscreen mode change. - */ - void onFullScreenChanged(boolean fullScreen); - } - - @Override - protected void onAttachedToWindow() { - mProvider.onAttachedToWindow_impl(); - } - - @Override - protected void onDetachedFromWindow() { - mProvider.onDetachedFromWindow_impl(); - } - - @Override - public CharSequence getAccessibilityClassName() { - return mProvider.getAccessibilityClassName_impl(); + void onCustomAction(String action, Bundle extras); } @Override - public boolean onTouchEvent(MotionEvent ev) { - return mProvider.onTouchEvent_impl(ev); - } - - @Override - public boolean onTrackballEvent(MotionEvent ev) { - return mProvider.onTrackballEvent_impl(ev); - } - - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { - return mProvider.onKeyDown_impl(keyCode, event); - } - - @Override - public void onFinishInflate() { - mProvider.onFinishInflate_impl(); - } - - @Override - public boolean dispatchKeyEvent(KeyEvent event) { - return mProvider.dispatchKeyEvent_impl(event); - } - - @Override - public void setEnabled(boolean enabled) { - mProvider.setEnabled_impl(enabled); - } - - private class SuperProvider implements ViewProvider { - @Override - public void onAttachedToWindow_impl() { - VideoView2.super.onAttachedToWindow(); - } - - @Override - public void onDetachedFromWindow_impl() { - VideoView2.super.onDetachedFromWindow(); - } - - @Override - public CharSequence getAccessibilityClassName_impl() { - return VideoView2.super.getAccessibilityClassName(); - } - - @Override - public boolean onTouchEvent_impl(MotionEvent ev) { - return VideoView2.super.onTouchEvent(ev); - } - - @Override - public boolean onTrackballEvent_impl(MotionEvent ev) { - return VideoView2.super.onTrackballEvent(ev); - } - - @Override - public boolean onKeyDown_impl(int keyCode, KeyEvent event) { - return VideoView2.super.onKeyDown(keyCode, event); - } - - @Override - public void onFinishInflate_impl() { - VideoView2.super.onFinishInflate(); - } - - @Override - public boolean dispatchKeyEvent_impl(KeyEvent event) { - return VideoView2.super.dispatchKeyEvent(event); - } - - @Override - public void setEnabled_impl(boolean enabled) { - VideoView2.super.setEnabled(enabled); - } + protected void onLayout(boolean changed, int l, int t, int r, int b) { + mProvider.onLayout_impl(changed, l, t, r, b); } } |