summaryrefslogtreecommitdiff
path: root/android/widget
diff options
context:
space:
mode:
authorJustin Klaassen <justinklaassen@google.com>2018-04-03 23:21:57 -0400
committerJustin Klaassen <justinklaassen@google.com>2018-04-03 23:21:57 -0400
commit4d01eeaffaa720e4458a118baa137a11614f00f7 (patch)
tree66751893566986236788e3c796a7cc5e90d05f52 /android/widget
parenta192cc2a132cb0ee8588e2df755563ec7008c179 (diff)
downloadandroid-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')
-rw-r--r--android/widget/AbsListView.java10
-rw-r--r--android/widget/AbsSeekBar.java2
-rw-r--r--android/widget/AdapterView.java2
-rw-r--r--android/widget/CheckedTextView.java2
-rw-r--r--android/widget/CompoundButton.java2
-rw-r--r--android/widget/Editor.java405
-rw-r--r--android/widget/GridLayout.java6
-rw-r--r--android/widget/ImageView.java23
-rw-r--r--android/widget/LinearLayout.java4
-rw-r--r--android/widget/Magnifier.java654
-rw-r--r--android/widget/MediaControlView2.java280
-rw-r--r--android/widget/PopupWindow.java6
-rw-r--r--android/widget/RadioGroup.java10
-rw-r--r--android/widget/RelativeLayout.java4
-rw-r--r--android/widget/RemoteViews.java9
-rw-r--r--android/widget/SearchView.java29
-rw-r--r--android/widget/SelectionActionModeHelper.java278
-rw-r--r--android/widget/SmartSelectSprite.java168
-rw-r--r--android/widget/TextInputTimePickerView.java7
-rw-r--r--android/widget/TextView.java465
-rw-r--r--android/widget/TextViewPrecomputedTextPerfTest.java455
-rw-r--r--android/widget/TimePickerClockDelegate.java57
-rw-r--r--android/widget/Toast.java8
-rw-r--r--android/widget/VideoView2.java459
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);
}
}