From a192cc2a132cb0ee8588e2df755563ec7008c179 Mon Sep 17 00:00:00 2001 From: Jeff Davidson Date: Thu, 8 Feb 2018 15:30:06 -0800 Subject: Update fullsdk to 4575844 /google/data/ro/projects/android/fetch_artifact \ --bid 4575844 \ --target sdk_phone_x86_64-sdk \ sdk-repo-linux-sources-4575844.zip Test: TreeHugger Change-Id: I81e0eb157b4ac3b38408d0ef86f9d6286471f87a --- android/widget/AbsListView.java | 19 +- android/widget/AdapterView.java | 2 +- android/widget/CheckedTextView.java | 2 +- android/widget/CompoundButton.java | 2 +- android/widget/EditText.java | 4 + android/widget/Editor.java | 44 +- android/widget/Magnifier.java | 41 +- android/widget/MediaControlView2.java | 279 ++++++++++++ android/widget/SelectionActionModeHelper.java | 34 +- android/widget/TextView.java | 314 ++++++++++++-- android/widget/VideoView2.java | 602 ++++++++++++++++++++++++++ 11 files changed, 1275 insertions(+), 68 deletions(-) create mode 100644 android/widget/MediaControlView2.java create mode 100644 android/widget/VideoView2.java (limited to 'android/widget') diff --git a/android/widget/AbsListView.java b/android/widget/AbsListView.java index e0c897d3..594d2400 100644 --- a/android/widget/AbsListView.java +++ b/android/widget/AbsListView.java @@ -19,6 +19,7 @@ package android.widget; import android.annotation.ColorInt; import android.annotation.DrawableRes; import android.annotation.NonNull; +import android.annotation.TestApi; import android.content.Context; import android.content.Intent; import android.content.res.Configuration; @@ -30,6 +31,7 @@ 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; @@ -2744,13 +2746,21 @@ public abstract class AbsListView extends AdapterView implements Te } private void drawSelector(Canvas canvas) { - if (!mSelectorRect.isEmpty()) { + if (shouldDrawSelector()) { final Drawable selector = mSelector; selector.setBounds(mSelectorRect); selector.draw(canvas); } } + /** + * @hide + */ + @TestApi + public final boolean shouldDrawSelector() { + return !mSelectorRect.isEmpty(); + } + /** * Controls whether the selection highlight drawable should be drawn on top of the item or * behind it. @@ -6026,6 +6036,11 @@ public abstract class AbsListView extends AdapterView 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); + } } /** @@ -6849,7 +6864,7 @@ public abstract class AbsListView extends AdapterView 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. - notifyViewAccessibilityStateChangedIfNeeded( + notifyAccessibilityStateChanged( AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE); // Don't scrap views that have transient state. diff --git a/android/widget/AdapterView.java b/android/widget/AdapterView.java index 6c192563..08374cb1 100644 --- a/android/widget/AdapterView.java +++ b/android/widget/AdapterView.java @@ -1093,7 +1093,7 @@ public abstract class AdapterView extends ViewGroup { checkSelectionChanged(); } - notifySubtreeAccessibilityStateChangedIfNeeded(); + notifyAccessibilitySubtreeChanged(); } /** diff --git a/android/widget/CheckedTextView.java b/android/widget/CheckedTextView.java index 92bfd56d..af01a3eb 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(); - notifyViewAccessibilityStateChangedIfNeeded( + notifyAccessibilityStateChanged( AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED); } } diff --git a/android/widget/CompoundButton.java b/android/widget/CompoundButton.java index 0762b156..e57f1536 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(); - notifyViewAccessibilityStateChangedIfNeeded( + notifyAccessibilityStateChanged( AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED); // Avoid infinite recursions if setChecked() is called from a listener diff --git a/android/widget/EditText.java b/android/widget/EditText.java index 336c20cd..728824c2 100644 --- a/android/widget/EditText.java +++ b/android/widget/EditText.java @@ -106,6 +106,10 @@ public class EditText extends TextView { @Override public Editable getText() { CharSequence text = super.getText(); + // This can only happen during construction. + if (text == null) { + return null; + } if (text instanceof Editable) { return (Editable) super.getText(); } diff --git a/android/widget/Editor.java b/android/widget/Editor.java index 05d18d18..7bb0db1c 100644 --- a/android/widget/Editor.java +++ b/android/widget/Editor.java @@ -2095,14 +2095,7 @@ public class Editor { if (!(mTextView.getText() instanceof Spannable)) { return; } - Spannable text = (Spannable) mTextView.getText(); stopTextActionMode(); - if (mTextView.isTextSelectable()) { - Selection.setSelection((Spannable) text, link.getStart(), link.getEnd()); - } else { - //TODO: Nonselectable text - } - getSelectionActionModeHelper().startLinkActionModeAsync(link); } @@ -2179,7 +2172,8 @@ public class Editor { return false; } - if (!checkField() || !mTextView.hasSelection()) { + if (actionMode != TextActionMode.TEXT_LINK + && (!checkField() || !mTextView.hasSelection())) { return false; } @@ -3679,6 +3673,8 @@ public class Editor { mIsShowingUp = true; super.show(); } + + mSuggestionListView.setVisibility(mNumberOfSuggestions == 0 ? View.GONE : View.VISIBLE); } @Override @@ -4012,7 +4008,6 @@ public class Editor { if (isValidAssistMenuItem( textClassification.getIcon(), textClassification.getLabel(), - textClassification.getOnClickListener(), textClassification.getIntent())) { final MenuItem item = menu.add( TextView.ID_ASSIST, TextView.ID_ASSIST, MENU_ITEM_ORDER_ASSIST, @@ -4020,14 +4015,15 @@ public class Editor { .setIcon(textClassification.getIcon()) .setIntent(textClassification.getIntent()); item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); - mAssistClickHandlers.put(item, textClassification.getOnClickListener()); + 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.getSecondaryOnClickListener(i), textClassification.getSecondaryIntent(i))) { continue; } @@ -4038,7 +4034,9 @@ public class Editor { .setIcon(textClassification.getSecondaryIcon(i)) .setIntent(textClassification.getSecondaryIntent(i)); item.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); - mAssistClickHandlers.put(item, textClassification.getSecondaryOnClickListener(i)); + mAssistClickHandlers.put(item, + TextClassification.createStartActivityOnClickListener( + mTextView.getContext(), textClassification.getSecondaryIntent(i))); } } @@ -4054,10 +4052,9 @@ public class Editor { } } - private boolean isValidAssistMenuItem( - Drawable icon, CharSequence label, OnClickListener onClick, Intent intent) { + private boolean isValidAssistMenuItem(Drawable icon, CharSequence label, Intent intent) { final boolean hasUi = icon != null || !TextUtils.isEmpty(label); - final boolean hasAction = onClick != null || isSupportedIntent(intent); + final boolean hasAction = isSupportedIntent(intent); return hasUi && hasAction; } @@ -4632,7 +4629,7 @@ public class Editor { return 0; } - protected final void showMagnifier() { + protected final void showMagnifier(@NonNull final MotionEvent event) { if (mMagnifier == null) { return; } @@ -4658,9 +4655,10 @@ public class Editor { final Layout layout = mTextView.getLayout(); final int lineNumber = layout.getLineForOffset(offset); - // Horizontally snap to character offset. - final float xPosInView = getHorizontal(mTextView.getLayout(), offset) - + mTextView.getTotalPaddingLeft() - mTextView.getScrollX(); + // Horizontally move the magnifier smoothly. + final int[] textViewLocationOnScreen = new int[2]; + mTextView.getLocationOnScreen(textViewLocationOnScreen); + final float xPosInView = event.getRawX() - textViewLocationOnScreen[0]; // Vertically snap to middle of current line. final float yPosInView = (mTextView.getLayout().getLineTop(lineNumber) + mTextView.getLayout().getLineBottom(lineNumber)) / 2.0f @@ -4855,11 +4853,11 @@ public class Editor { case MotionEvent.ACTION_DOWN: mDownPositionX = ev.getRawX(); mDownPositionY = ev.getRawY(); - showMagnifier(); + showMagnifier(ev); break; case MotionEvent.ACTION_MOVE: - showMagnifier(); + showMagnifier(ev); break; case MotionEvent.ACTION_UP: @@ -5213,11 +5211,11 @@ public class Editor { // re-engages the handle. mTouchWordDelta = 0.0f; mPrevX = UNSET_X_VALUE; - showMagnifier(); + showMagnifier(event); break; case MotionEvent.ACTION_MOVE: - showMagnifier(); + showMagnifier(event); break; case MotionEvent.ACTION_UP: diff --git a/android/widget/Magnifier.java b/android/widget/Magnifier.java index 26dfcc2d..310b1708 100644 --- a/android/widget/Magnifier.java +++ b/android/widget/Magnifier.java @@ -32,6 +32,7 @@ import android.view.PixelCopy; import android.view.Surface; import android.view.SurfaceView; import android.view.View; +import android.view.ViewParent; import com.android.internal.util.Preconditions; @@ -44,6 +45,8 @@ public final class Magnifier { private static final int NONEXISTENT_PREVIOUS_CONFIG_VALUE = -1; // 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; // The center coordinates of the window containing the magnifier. @@ -87,6 +90,8 @@ public final class Magnifier { com.android.internal.R.dimen.magnifier_height); mZoomScale = context.getResources().getFloat( com.android.internal.R.dimen.magnifier_zoom_scale); + // 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); @@ -120,9 +125,34 @@ public final class Magnifier { configureCoordinates(xPosInView, yPosInView); // Clamp startX value to avoid distorting the rendering of the magnifier content. - final int startX = Math.max(0, Math.min( + // 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, - mView.getWidth() - mBitmap.getWidth())); + zeroScrollXInSurface + actualWidth - mBitmap.getWidth())); final int startY = mCenterZoomCoords.y - mBitmap.getHeight() / 2; if (xPosInView != mPrevPosInView.x || yPosInView != mPrevPosInView.y) { @@ -169,10 +199,9 @@ public final class Magnifier { posX = xPosInView; posY = yPosInView; } else { - final int[] coordinatesInSurface = new int[2]; - mView.getLocationInSurface(coordinatesInSurface); - posX = xPosInView + coordinatesInSurface[0]; - posY = yPosInView + coordinatesInSurface[1]; + mView.getLocationInSurface(mViewCoordinatesInSurface); + posX = xPosInView + mViewCoordinatesInSurface[0]; + posY = yPosInView + mViewCoordinatesInSurface[1]; } mCenterZoomCoords.x = Math.round(posX); diff --git a/android/widget/MediaControlView2.java b/android/widget/MediaControlView2.java new file mode 100644 index 00000000..f1d633a2 --- /dev/null +++ b/android/widget/MediaControlView2.java @@ -0,0 +1,279 @@ +/* + * Copyright (C) 2017 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 android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.media.session.MediaController; +import android.media.update.ApiLoader; +import android.media.update.MediaControlView2Provider; +import android.media.update.ViewProvider; +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; +/** + * 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. + * + *

+ * MediaControlView2 can be initialized in two different ways: + * 1) When VideoView2 is initialized, it automatically initializes a MediaControlView2 instance and + * 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, + * 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). + * + * TODO PUBLIC API + * @hide + */ +public class MediaControlView2 extends FrameLayout { + /** @hide */ + @IntDef({ + BUTTON_PLAY_PAUSE, + BUTTON_FFWD, + BUTTON_REW, + BUTTON_NEXT, + BUTTON_PREV, + BUTTON_SUBTITLE, + BUTTON_FULL_SCREEN, + BUTTON_OVERFLOW, + BUTTON_MUTE, + BUTTON_ASPECT_RATIO, + BUTTON_SETTINGS + }) + @Retention(RetentionPolicy.SOURCE) + public @interface Button {} + + public static final int BUTTON_PLAY_PAUSE = 1; + public static final int BUTTON_FFWD = 2; + public static final int BUTTON_REW = 3; + public static final int BUTTON_NEXT = 4; + public static final int BUTTON_PREV = 5; + public static final int BUTTON_SUBTITLE = 6; + public static final int BUTTON_FULL_SCREEN = 7; + public static final int BUTTON_OVERFLOW = 8; + public static final int BUTTON_MUTE = 9; + public static final int BUTTON_ASPECT_RATIO = 10; + public static final int BUTTON_SETTINGS = 11; + + private final MediaControlView2Provider mProvider; + + public MediaControlView2(@NonNull Context context) { + this(context, null); + } + + public MediaControlView2(@NonNull Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public MediaControlView2(@NonNull Context context, @Nullable AttributeSet attrs, + 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(); + } + + /** + * Hide the control view from the screen. + */ + public void hide() { + mProvider.hide_impl(); + } + + /** + * 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. + */ + public void showSubtitle() { + mProvider.showSubtitle_impl(); + } + + /** + * Hides the currently displayed subtitle. + */ + public void hideSubtitle() { + mProvider.hideSubtitle_impl(); + } + + /** + * 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. + * + * @param next Listener for clicking next button + * @param prev Listener for clicking previous button + */ + public void setPrevNextListeners(View.OnClickListener next, View.OnClickListener prev) { + mProvider.setPrevNextListeners_impl(next, prev); + } + + /** + * 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 + */ + 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(); + } + + @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() { + 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); + } + } +} diff --git a/android/widget/SelectionActionModeHelper.java b/android/widget/SelectionActionModeHelper.java index 2c6466cd..3bfa520c 100644 --- a/android/widget/SelectionActionModeHelper.java +++ b/android/widget/SelectionActionModeHelper.java @@ -235,10 +235,13 @@ 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())) { + && (mTextView.isTextSelectable() + || mTextView.isTextEditable() + || actionMode == Editor.TextActionMode.TEXT_LINK)) { // Do not change the selection if TextClassifier should be dark launched. if (!mTextView.getTextClassifier().getSettings().isDarkLaunch()) { Selection.setSelection((Spannable) text, result.mStart, result.mEnd); + mTextView.invalidate(); } mTextClassification = result.mClassification; } else { @@ -250,8 +253,17 @@ public final class SelectionActionModeHelper { && (mTextView.isTextSelectable() || mTextView.isTextEditable())) { controller.show(); } - if (result != null && actionMode == Editor.TextActionMode.SELECTION) { - mSelectionTracker.onSmartSelection(result); + if (result != null) { + switch (actionMode) { + case Editor.TextActionMode.SELECTION: + mSelectionTracker.onSmartSelection(result); + break; + case Editor.TextActionMode.TEXT_LINK: + mSelectionTracker.onLinkSelected(result); + break; + default: + break; + } } } mEditor.setRestartActionModeOnNextRefresh(false); @@ -486,12 +498,24 @@ public final class SelectionActionModeHelper { * Called when selection action mode is started and the results come from a classifier. */ public void onSmartSelection(SelectionResult result) { + onClassifiedSelection(result); + mLogger.logSelectionModified( + result.mStart, result.mEnd, result.mClassification, result.mSelection); + } + + /** + * Called when link action mode is started and the classification comes from a classifier. + */ + public void onLinkSelected(SelectionResult result) { + onClassifiedSelection(result); + // TODO: log (b/70246800) + } + + private void onClassifiedSelection(SelectionResult result) { if (isSelectionStarted()) { mSelectionStart = result.mStart; mSelectionEnd = result.mEnd; mAllowReset = mSelectionStart != mOriginalStart || mSelectionEnd != mOriginalEnd; - mLogger.logSelectionModified( - result.mStart, result.mEnd, result.mClassification, result.mSelection); } } diff --git a/android/widget/TextView.java b/android/widget/TextView.java index 1e17f34a..7d3fcf46 100644 --- a/android/widget/TextView.java +++ b/android/widget/TextView.java @@ -27,8 +27,10 @@ import android.annotation.ColorInt; import android.annotation.DrawableRes; import android.annotation.FloatRange; import android.annotation.IntDef; +import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.Px; import android.annotation.Size; import android.annotation.StringRes; import android.annotation.StyleRes; @@ -44,6 +46,7 @@ import android.content.UndoManager; import android.content.res.ColorStateList; import android.content.res.CompatibilityInfo; import android.content.res.Configuration; +import android.content.res.ResourceId; import android.content.res.Resources; import android.content.res.TypedArray; import android.content.res.XmlResourceParser; @@ -51,6 +54,7 @@ import android.graphics.BaseCanvas; import android.graphics.Canvas; import android.graphics.Insets; import android.graphics.Paint; +import android.graphics.Paint.FontMetricsInt; import android.graphics.Path; import android.graphics.PorterDuff; import android.graphics.Rect; @@ -76,8 +80,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.PremeasuredText; import android.text.Selection; import android.text.SpanWatcher; import android.text.Spannable; @@ -295,6 +299,7 @@ import java.util.Locale; * @attr ref android.R.styleable#TextView_imeActionId * @attr ref android.R.styleable#TextView_editorExtras * @attr ref android.R.styleable#TextView_elegantTextHeight + * @attr ref android.R.styleable#TextView_fallbackLineSpacing * @attr ref android.R.styleable#TextView_letterSpacing * @attr ref android.R.styleable#TextView_fontFeatureSettings * @attr ref android.R.styleable#TextView_breakStrategy @@ -304,12 +309,12 @@ import java.util.Locale; * @attr ref android.R.styleable#TextView_autoSizeMaxTextSize * @attr ref android.R.styleable#TextView_autoSizeStepGranularity * @attr ref android.R.styleable#TextView_autoSizePresetSizes + * @attr ref android.R.styleable#TextView_accessibilityHeading */ @RemoteView public class TextView extends View implements ViewTreeObserver.OnPreDrawListener { static final String LOG_TAG = "TextView"; static final boolean DEBUG_EXTRACT = false; - static final boolean DEBUG_AUTOFILL = false; private static final float[] TEMP_POSITION = new float[2]; // Enum for the "typeface" XML parameter. @@ -399,6 +404,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private int mCurTextColor; private int mCurHintTextColor; private boolean mFreezesText; + private boolean mIsAccessibilityHeading; private Editable.Factory mEditableFactory = Editable.Factory.getInstance(); private Spannable.Factory mSpannableFactory = Spannable.Factory.getInstance(); @@ -654,7 +660,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener // True if internationalized input should be used for numbers and date and time. private final boolean mUseInternationalizedInput; // True if fallback fonts that end up getting used should be allowed to affect line spacing. - /* package */ final boolean mUseFallbackLineSpacing; + /* package */ boolean mUseFallbackLineSpacing; @ViewDebug.ExportedProperty(category = "text") private int mGravity = Gravity.TOP | Gravity.START; @@ -785,9 +791,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener // mAutoSizeStepGranularityInPx. private boolean mHasPresetAutoSizeValues = false; - // Indicates whether the text was set from resources or dynamically, so it can be used to + // Indicates whether the text was set statically or dynamically, so it can be used to // sanitize autofill requests. - private boolean mTextFromResource = false; + private boolean mTextSetFromXmlOrResourceId = false; + // Resource id used to set the text - used for autofill purposes. + private @StringRes int mTextId = ResourceId.ID_NULL; /** * Kick-start the font cache for the zygote process (to pay the cost of @@ -921,12 +929,16 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener int inputType = EditorInfo.TYPE_NULL; a = theme.obtainStyledAttributes( attrs, com.android.internal.R.styleable.TextView, defStyleAttr, defStyleRes); + int firstBaselineToTopHeight = -1; + int lastBaselineToBottomHeight = -1; + int lineHeight = -1; readTextAppearance(context, a, attributes, true /* styleArray */); int n = a.getIndexCount(); - boolean fromResourceId = false; + // Must set id in a temporary variable because it will be reset by setText() + boolean textIsSetFromXml = false; for (int i = 0; i < n; i++) { int attr = a.getIndex(i); @@ -1068,7 +1080,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener break; case com.android.internal.R.styleable.TextView_text: - fromResourceId = true; + textIsSetFromXml = true; + mTextId = a.getResourceId(attr, ResourceId.ID_NULL); text = a.getText(attr); break; @@ -1244,6 +1257,20 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener case com.android.internal.R.styleable.TextView_justificationMode: mJustificationMode = a.getInt(attr, Layout.JUSTIFICATION_MODE_NONE); break; + + case com.android.internal.R.styleable.TextView_firstBaselineToTopHeight: + firstBaselineToTopHeight = a.getDimensionPixelSize(attr, -1); + break; + + case com.android.internal.R.styleable.TextView_lastBaselineToBottomHeight: + lastBaselineToBottomHeight = a.getDimensionPixelSize(attr, -1); + break; + + case com.android.internal.R.styleable.TextView_lineHeight: + lineHeight = a.getDimensionPixelSize(attr, -1); + break; + case com.android.internal.R.styleable.TextView_accessibilityHeading: + mIsAccessibilityHeading = a.getBoolean(attr, false); } } @@ -1460,8 +1487,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } setText(text, bufferType); - if (fromResourceId) { - mTextFromResource = true; + if (textIsSetFromXml) { + mTextSetFromXmlOrResourceId = true; } if (hint != null) setHint(hint); @@ -1558,6 +1585,16 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } else { mAutoSizeTextType = AUTO_SIZE_TEXT_TYPE_NONE; } + + if (firstBaselineToTopHeight >= 0) { + setFirstBaselineToTopHeight(firstBaselineToTopHeight); + } + if (lastBaselineToBottomHeight >= 0) { + setLastBaselineToBottomHeight(lastBaselineToBottomHeight); + } + if (lineHeight >= 0) { + setLineHeight(lineHeight); + } } /** @@ -2360,7 +2397,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener setText(mText); if (hasPasswordTransformationMethod()) { - notifyViewAccessibilityStateChangedIfNeeded( + notifyAccessibilityStateChanged( AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED); } @@ -3160,6 +3197,12 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } + /** + * @inheritDoc + * + * @see #setFirstBaselineToTopHeight(int) + * @see #setLastBaselineToBottomHeight(int) + */ @Override public void setPadding(int left, int top, int right, int bottom) { if (left != mPaddingLeft @@ -3174,6 +3217,12 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener invalidate(); } + /** + * @inheritDoc + * + * @see #setFirstBaselineToTopHeight(int) + * @see #setLastBaselineToBottomHeight(int) + */ @Override public void setPaddingRelative(int start, int top, int end, int bottom) { if (start != getPaddingStart() @@ -3188,6 +3237,97 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener invalidate(); } + /** + * Updates the top padding of the TextView so that {@code firstBaselineToTopHeight} is + * equal to the distance between the firt text baseline and the top of this TextView. + * Note that if {@code FontMetrics.top} or {@code FontMetrics.ascent} was + * already greater than {@code firstBaselineToTopHeight}, the top padding is not updated. + * + * @param firstBaselineToTopHeight distance between first baseline to top of the container + * in pixels + * + * @see #getFirstBaselineToTopHeight() + * @see #setPadding(int, int, int, int) + * @see #setPaddingRelative(int, int, int, int) + * + * @attr ref android.R.styleable#TextView_firstBaselineToTopHeight + */ + public void setFirstBaselineToTopHeight(@Px @IntRange(from = 0) int firstBaselineToTopHeight) { + Preconditions.checkArgumentNonnegative(firstBaselineToTopHeight); + + final FontMetricsInt fontMetrics = getPaint().getFontMetricsInt(); + final int fontMetricsTop; + if (getIncludeFontPadding()) { + fontMetricsTop = fontMetrics.top; + } else { + fontMetricsTop = fontMetrics.ascent; + } + + // TODO: Decide if we want to ignore density ratio (i.e. when the user changes font size + // in settings). At the moment, we don't. + + if (firstBaselineToTopHeight > Math.abs(fontMetricsTop)) { + final int paddingTop = firstBaselineToTopHeight - (-fontMetricsTop); + setPadding(getPaddingLeft(), paddingTop, getPaddingRight(), getPaddingBottom()); + } + } + + /** + * Updates the bottom padding of the TextView so that {@code lastBaselineToBottomHeight} is + * equal to the distance between the last text baseline and the bottom of this TextView. + * Note that if {@code FontMetrics.bottom} or {@code FontMetrics.descent} was + * already greater than {@code lastBaselineToBottomHeight}, the bottom padding is not updated. + * + * @param lastBaselineToBottomHeight distance between last baseline to bottom of the container + * in pixels + * + * @see #getLastBaselineToBottomHeight() + * @see #setPadding(int, int, int, int) + * @see #setPaddingRelative(int, int, int, int) + * + * @attr ref android.R.styleable#TextView_lastBaselineToBottomHeight + */ + public void setLastBaselineToBottomHeight( + @Px @IntRange(from = 0) int lastBaselineToBottomHeight) { + Preconditions.checkArgumentNonnegative(lastBaselineToBottomHeight); + + final FontMetricsInt fontMetrics = getPaint().getFontMetricsInt(); + final int fontMetricsBottom; + if (getIncludeFontPadding()) { + fontMetricsBottom = fontMetrics.bottom; + } else { + fontMetricsBottom = fontMetrics.descent; + } + + // TODO: Decide if we want to ignore density ratio (i.e. when the user changes font size + // in settings). At the moment, we don't. + + if (lastBaselineToBottomHeight > Math.abs(fontMetricsBottom)) { + final int paddingBottom = lastBaselineToBottomHeight - fontMetricsBottom; + setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), paddingBottom); + } + } + + /** + * Returns the distance between the first text baseline and the top of this TextView. + * + * @see #setFirstBaselineToTopHeight(int) + * @attr ref android.R.styleable#TextView_firstBaselineToTopHeight + */ + public int getFirstBaselineToTopHeight() { + return getPaddingTop() - getPaint().getFontMetricsInt().top; + } + + /** + * Returns the distance between the last text baseline and the bottom of this TextView. + * + * @see #setLastBaselineToBottomHeight(int) + * @attr ref android.R.styleable#TextView_lastBaselineToBottomHeight + */ + public int getLastBaselineToBottomHeight() { + return getPaddingBottom() + getPaint().getFontMetricsInt().bottom; + } + /** * Gets the autolink mask of the text. See {@link * android.text.util.Linkify#ALL Linkify.ALL} and peers for @@ -3250,6 +3390,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener float mShadowDx = 0, mShadowDy = 0, mShadowRadius = 0; boolean mHasElegant = false; boolean mElegant = false; + boolean mHasFallbackLineSpacing = false; + boolean mFallbackLineSpacing = false; boolean mHasLetterSpacing = false; float mLetterSpacing = 0; String mFontFeatureSettings = null; @@ -3274,6 +3416,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener + " mShadowRadius:" + mShadowRadius + "\n" + " mHasElegant:" + mHasElegant + "\n" + " mElegant:" + mElegant + "\n" + + " mHasFallbackLineSpacing:" + mHasFallbackLineSpacing + "\n" + + " mFallbackLineSpacing:" + mFallbackLineSpacing + "\n" + " mHasLetterSpacing:" + mHasLetterSpacing + "\n" + " mLetterSpacing:" + mLetterSpacing + "\n" + " mFontFeatureSettings:" + mFontFeatureSettings + "\n" @@ -3312,6 +3456,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener com.android.internal.R.styleable.TextAppearance_shadowRadius); sAppearanceValues.put(com.android.internal.R.styleable.TextView_elegantTextHeight, com.android.internal.R.styleable.TextAppearance_elegantTextHeight); + sAppearanceValues.put(com.android.internal.R.styleable.TextView_fallbackLineSpacing, + com.android.internal.R.styleable.TextAppearance_fallbackLineSpacing); sAppearanceValues.put(com.android.internal.R.styleable.TextView_letterSpacing, com.android.internal.R.styleable.TextAppearance_letterSpacing); sAppearanceValues.put(com.android.internal.R.styleable.TextView_fontFeatureSettings, @@ -3402,6 +3548,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener attributes.mHasElegant = true; attributes.mElegant = appearance.getBoolean(attr, attributes.mElegant); break; + case com.android.internal.R.styleable.TextAppearance_fallbackLineSpacing: + attributes.mHasFallbackLineSpacing = true; + attributes.mFallbackLineSpacing = appearance.getBoolean(attr, + attributes.mFallbackLineSpacing); + break; case com.android.internal.R.styleable.TextAppearance_letterSpacing: attributes.mHasLetterSpacing = true; attributes.mLetterSpacing = @@ -3455,6 +3606,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener setElegantTextHeight(attributes.mElegant); } + if (attributes.mHasFallbackLineSpacing) { + setFallbackLineSpacing(attributes.mFallbackLineSpacing); + } + if (attributes.mHasLetterSpacing) { setLetterSpacing(attributes.mLetterSpacing); } @@ -3736,7 +3891,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * * @param elegant set the paint's elegant metrics flag. * - * @see Paint#isElegantTextHeight(boolean) + * @see #isElegantTextHeight() + * @see Paint#isElegantTextHeight() * * @attr ref android.R.styleable#TextView_elegantTextHeight */ @@ -3751,6 +3907,43 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } + /** + * Set whether to respect the ascent and descent of the fallback fonts that are used in + * displaying the text (which is needed to avoid text from consecutive lines running into + * each other). If set, fallback fonts that end up getting used can increase the ascent + * and descent of the lines that they are used on. + *

+ * It is required to be true if text could be in languages like Burmese or Tibetan where text + * is typically much taller or deeper than Latin text. + * + * @param enabled whether to expand linespacing based on fallback fonts, {@code true} by default + * + * @see StaticLayout.Builder#setUseLineSpacingFromFallbacks(boolean) + * + * @attr ref android.R.styleable#TextView_fallbackLineSpacing + */ + public void setFallbackLineSpacing(boolean enabled) { + if (mUseFallbackLineSpacing != enabled) { + mUseFallbackLineSpacing = enabled; + if (mLayout != null) { + nullLayouts(); + requestLayout(); + invalidate(); + } + } + } + + /** + * @return whether fallback line spacing is enabled, {@code true} by default + * + * @see #setFallbackLineSpacing(boolean) + * + * @attr ref android.R.styleable#TextView_fallbackLineSpacing + */ + public boolean isFallbackLineSpacing() { + return mUseFallbackLineSpacing; + } + /** * Get the value of the TextView's elegant height metrics flag. This setting selects font * variants that have not been compacted to fit Latin-based vertical @@ -4916,6 +5109,53 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return mSpacingAdd; } + /** + * Sets an explicit line height for this TextView. This is equivalent to the vertical distance + * between subsequent baselines in the TextView. + * + * @param lineHeight the line height in pixels + * + * @see #setLineSpacing(float, float) + * @see #getLineSpacing() + * + * @attr ref android.R.styleable#TextView_lineHeight + */ + public void setLineHeight(@Px @IntRange(from = 0) int lineHeight) { + Preconditions.checkArgumentNonnegative(lineHeight); + + final int fontHeight = getPaint().getFontMetricsInt(null); + // Make sure we don't setLineSpacing if it's not needed to avoid unnecessary redraw. + if (lineHeight != fontHeight) { + // Set lineSpacingExtra by the difference of lineSpacing with lineHeight + setLineSpacing(lineHeight - fontHeight, 1f); + } + } + + /** + * Gets whether this view is a heading for accessibility purposes. + * + * @return {@code true} if the view is a heading, {@code false} otherwise. + * + * @attr ref android.R.styleable#TextView_accessibilityHeading + */ + public boolean isAccessibilityHeading() { + return mIsAccessibilityHeading; + } + + /** + * Set if view is a heading for a section of content for accessibility purposes. + * + * @param isHeading {@code true} if the view is a heading, {@code false} otherwise. + * + * @attr ref android.R.styleable#TextView_accessibilityHeading + */ + public void setAccessibilityHeading(boolean isHeading) { + if (isHeading != mIsAccessibilityHeading) { + mIsAccessibilityHeading = isHeading; + notifyAccessibilityStateChanged(AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED); + } + } + /** * Convenience method to append the specified text to the TextView's * display buffer, upgrading it to {@link android.widget.TextView.BufferType#EDITABLE} @@ -5278,7 +5518,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private void setText(CharSequence text, BufferType type, boolean notifyBefore, int oldlen) { - mTextFromResource = false; + mTextSetFromXmlOrResourceId = false; if (text == null) { text = ""; } @@ -5336,7 +5576,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (imm != null) imm.restartInput(this); } else if (type == BufferType.SPANNABLE || mMovement != null) { text = mSpannableFactory.newSpannable(text); - } else if (!(text instanceof PremeasuredText || text instanceof CharWrapper)) { + } else if (!(text instanceof MeasuredText || text instanceof CharWrapper)) { text = TextUtils.stringOrSpannedString(text); } @@ -5419,7 +5659,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener sendOnTextChanged(text, 0, oldlen, textLength); onTextChanged(text, 0, oldlen, textLength); - notifyViewAccessibilityStateChangedIfNeeded(AccessibilityEvent.CONTENT_CHANGE_TYPE_TEXT); + notifyAccessibilityStateChanged(AccessibilityEvent.CONTENT_CHANGE_TYPE_TEXT); if (needEditableForNotification) { sendAfterTextChanged((Editable) text); @@ -5516,7 +5756,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener @android.view.RemotableViewMethod public final void setText(@StringRes int resid) { setText(getContext().getResources().getText(resid)); - mTextFromResource = true; + mTextSetFromXmlOrResourceId = true; + mTextId = resid; } /** @@ -5543,7 +5784,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener */ public final void setText(@StringRes int resid, BufferType type) { setText(getContext().getResources().getText(resid), type); - mTextFromResource = true; + mTextSetFromXmlOrResourceId = true; + mTextId = resid; } /** @@ -6151,7 +6393,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener public void setError(CharSequence error, Drawable icon) { createEditorIfNeeded(); mEditor.setError(error, icon); - notifyViewAccessibilityStateChangedIfNeeded( + notifyAccessibilityStateChanged( AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED); } @@ -9066,8 +9308,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener /** * - * Checks whether the transformation method applied to this TextView is set to ALL CAPS. This - * settings is internally ignored if this field is editable or selectable. + * Checks whether the transformation method applied to this TextView is set to ALL CAPS. * @return Whether the current transformation method is for ALL CAPS. * * @see #setAllCaps(boolean) @@ -9456,7 +9697,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } final AutofillManager afm = mContext.getSystemService(AutofillManager.class); if (afm != null) { - if (DEBUG_AUTOFILL) { + if (android.view.autofill.Helper.sVerbose) { Log.v(LOG_TAG, "sendAfterTextChanged(): notify AFM for text=" + mText); } afm.notifyValueChanged(TextView.this); @@ -10234,7 +10475,17 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener final boolean isPassword = hasPasswordTransformationMethod() || isPasswordInputType(getInputType()); if (forAutofill) { - structure.setDataIsSensitive(!mTextFromResource); + structure.setDataIsSensitive(!mTextSetFromXmlOrResourceId); + if (mTextId != ResourceId.ID_NULL) { + try { + structure.setTextIdEntry(getResources().getResourceEntryName(mTextId)); + } catch (Resources.NotFoundException e) { + if (android.view.autofill.Helper.sVerbose) { + Log.v(LOG_TAG, "onProvideAutofillStructure(): cannot set name for text id " + + mTextId + ": " + e.getMessage()); + } + } + } } if (!isPassword || forAutofill) { @@ -10455,6 +10706,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener info.setText(getTextForAccessibility()); info.setHintText(mHint); info.setShowingHintText(isShowingHint()); + info.setHeading(mIsAccessibilityHeading); if (mBufferType == BufferType.EDITABLE) { info.setEditable(true); @@ -10942,6 +11194,12 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return true; case ID_COPY: + // For link action mode in a non-selectable/non-focusable TextView, + // make sure that we set the appropriate min/max. + final int selStart = getSelectionStart(); + final int selEnd = getSelectionEnd(); + min = Math.max(0, Math.min(selStart, selEnd)); + max = Math.max(0, Math.max(selStart, selEnd)); final ClipData copyData = ClipData.newPlainText(null, getTransformedText(min, max)); if (setPrimaryClip(copyData)) { stopTextActionMode(); @@ -11164,11 +11422,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener */ public boolean requestActionMode(@NonNull TextLinks.TextLink link) { Preconditions.checkNotNull(link); - if (mEditor != null) { - mEditor.startLinkActionModeAsync(link); - return true; - } - return false; + createEditorIfNeeded(); + mEditor.startLinkActionModeAsync(link); + return true; } /** * @hide @@ -11883,7 +12139,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private final Choreographer mChoreographer; private byte mStatus = MARQUEE_STOPPED; - private final float mPixelsPerSecond; + private final float mPixelsPerMs; private float mMaxScroll; private float mMaxFadeScroll; private float mGhostStart; @@ -11896,7 +12152,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener Marquee(TextView v) { final float density = v.getContext().getResources().getDisplayMetrics().density; - mPixelsPerSecond = MARQUEE_DP_PER_SECOND * density; + mPixelsPerMs = MARQUEE_DP_PER_SECOND * density / 1000f; mView = new WeakReference(v); mChoreographer = Choreographer.getInstance(); } @@ -11941,7 +12197,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener long currentMs = mChoreographer.getFrameTime(); long deltaMs = currentMs - mLastAnimationMs; mLastAnimationMs = currentMs; - float deltaPx = deltaMs / 1000f * mPixelsPerSecond; + float deltaPx = deltaMs * mPixelsPerMs; mScroll += deltaPx; if (mScroll > mMaxScroll) { mScroll = mMaxScroll; diff --git a/android/widget/VideoView2.java b/android/widget/VideoView2.java new file mode 100644 index 00000000..8650c0a0 --- /dev/null +++ b/android/widget/VideoView2.java @@ -0,0 +1,602 @@ +/* + * Copyright 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 android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.media.AudioAttributes; +import android.media.AudioManager; +import android.media.MediaPlayerBase; +import android.media.update.ApiLoader; +import android.media.update.VideoView2Provider; +import android.media.update.ViewProvider; +import android.net.Uri; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.view.MotionEvent; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.List; +import java.util.Map; + +// 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. +/** + * Displays a video file. VideoView2 class is a View class which is wrapping MediaPlayer2 so that + * developers can easily implement a video rendering application. + * + *

+ * Data sources that VideoView2 supports : + * VideoView2 can play video files and audio-only fiels as + * well. It can load from various sources such as resources or content providers. The supported + * media file formats are the same as MediaPlayer2. + * + *

+ * View type can be selected : + * VideoView2 can render videos on top of TextureView as well as + * SurfaceView selectively. The default is SurfaceView and it can be changed using + * {@link #setViewType(int)} method. Using SurfaceView is recommended in most cases for saving + * battery. TextureView might be preferred for supporting various UIs such as animation and + * translucency. + * + *

+ * Differences between {@link VideoView} class : + * VideoView2 covers and inherits the most of + * VideoView's functionalities. The main differences are + *

    + *
  • VideoView2 inherits FrameLayout and renders videos using SurfaceView and TextureView + * selectively while VideoView inherits SurfaceView class. + *
  • VideoView2 is integrated with MediaControlView2 and a default MediaControlView2 instance is + * attached to VideoView2 by default. If a developer does not want to use the default + * MediaControlView2, needs to set enableControlView attribute to false. For instance, + *
    + * <VideoView2
    + *     android:id="@+id/video_view"
    + *     xmlns:widget="http://schemas.android.com/apk/com.android.media.update"
    + *     widget:enableControlView="false" />
    + * 
    + * 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}. + *
  • VideoView2 is integrated with MediaPlayer2 while VideoView is integrated with MediaPlayer. + *
  • 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 + * MediaControlView2 instance. + *

    + *
+ * + *

+ * Audio focus and audio attributes : + * By default, VideoView2 requests audio focus with + * {@link AudioManager#AUDIOFOCUS_GAIN}. Use {@link #setAudioFocusRequest(int)} to change this + * behavior. The default {@link AudioAttributes} used during playback have a usage of + * {@link AudioAttributes#USAGE_MEDIA} and a content type of + * {@link AudioAttributes#CONTENT_TYPE_MOVIE}, use {@link #setAudioAttributes(AudioAttributes)} to + * modify them. + * + *

+ * Note: VideoView2 does not retain its full state when going into the background. In particular, it + * 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 { + /** @hide */ + @IntDef({ + VIEW_TYPE_TEXTUREVIEW, + VIEW_TYPE_SURFACEVIEW + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ViewType {} + + public static final int VIEW_TYPE_SURFACEVIEW = 1; + public static final int VIEW_TYPE_TEXTUREVIEW = 2; + + private final VideoView2Provider mProvider; + + public VideoView2(@NonNull Context context) { + this(context, null); + } + + public VideoView2(@NonNull Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public VideoView2(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + 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; + } + + /** + * Sets MediaControlView2 instance. It will replace the previously assigned MediaControlView2 + * instance if any. + * + * @param mediaControlView a media control view2 instance. + */ + public void setMediaControlView2(MediaControlView2 mediaControlView) { + mProvider.setMediaControlView2_impl(mediaControlView); + } + + /** + * Returns MediaControlView2 instance which is currently attached to VideoView2 by default or by + * {@link #setMediaControlView2} method. + */ + public MediaControlView2 getMediaControlView2() { + return mProvider.getMediaControlView2_impl(); + } + + /** + * 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). + */ + public boolean isPlaying() { + return mProvider.isPlaying_impl(); + } + + // 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. + */ + public int getBufferPercentage() { + return mProvider.getBufferPercentage_impl(); + } + + /** + * Returns the audio session ID. + */ + public int getAudioSessionId() { + return mProvider.getAudioSessionId_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. + */ + public void showSubtitle() { + mProvider.showSubtitle_impl(); + } + + /** + * Stops showing closed captions or subtitles. + */ + public void hideSubtitle() { + mProvider.hideSubtitle_impl(); + } + + /** + * Sets full screen mode. + */ + public void setFullScreen(boolean fullScreen) { + mProvider.setFullScreen_impl(fullScreen); + } + + // TODO: This should be revised after integration with MediaPlayer2. + /** + * Sets playback speed. + * + * It is expressed as a multiplicative factor, where normal speed is 1.0f. If it is less than + * or equal to zero, it will be just ignored and nothing will be changed. If it exceeds the + * maximum speed that internal engine supports, system will determine best handling or it will + * be reset to the normal speed 1.0f. + * @param speed the playback speed. It should be positive. + */ + 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}, + * {@link AudioManager#AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK}, and + * {@link AudioManager#AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE}. Or use + * {@link AudioManager#AUDIOFOCUS_NONE} to express that audio focus should not be + * requested when playback starts. You can for instance use this when playing a silent animation + * through this class, and you don't want to affect other audio applications playing in the + * background. + * + * @param focusGain the type of audio focus gain that will be requested, or + * {@link AudioManager#AUDIOFOCUS_NONE} to disable the use audio focus during + * playback. + */ + public void setAudioFocusRequest(int focusGain) { + mProvider.setAudioFocusRequest_impl(focusGain); + } + + /** + * Sets the {@link AudioAttributes} to be used during the playback of the video. + * + * @param attributes non-null AudioAttributes. + */ + public void setAudioAttributes(@NonNull AudioAttributes attributes) { + mProvider.setAudioAttributes_impl(attributes); + } + + /** + * 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 routeCategories, + @Nullable MediaPlayerBase player) { + mProvider.setRouteAttributes_impl(routeCategories, player); + } + + /** + * Sets video path. + * + * @param path the path of the video. + */ + public void setVideoPath(String path) { + mProvider.setVideoPath_impl(path); + } + + /** + * Sets video URI. + * + * @param uri the URI of the video. + */ + public void setVideoURI(Uri uri) { + mProvider.setVideoURI_impl(uri); + } + + /** + * Sets video URI using specific headers. + * + * @param uri the URI of the video. + * @param headers the headers for the URI request. + * Note that the cross domain redirection is allowed by default, but that can be + * 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. + */ + public void setVideoURI(Uri uri, Map headers) { + mProvider.setVideoURI_impl(uri, headers); + } + + /** + * Selects which view will be used to render video between SurfacView and TextureView. + * + * @param viewType the view type to render video + *

    + *
  • {@link #VIEW_TYPE_SURFACEVIEW} + *
  • {@link #VIEW_TYPE_TEXTUREVIEW} + *
+ */ + public void setViewType(@ViewType int viewType) { + mProvider.setViewType_impl(viewType); + } + + /** + * Returns view type. + * + * @return view type. See {@see setViewType}. + */ + @ViewType + public int getViewType() { + return mProvider.getViewType_impl(); + } + + /** + * 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. + * + * @param l the callback that will be run. + */ + 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); + } + + /** + * Registers a callback to be invoked when a view type change is done. + * {@see #setViewType(int)} + * @param l The callback that will be run + */ + public void setOnViewTypeChangedListener(OnViewTypeChangedListener l) { + mProvider.setOnViewTypeChangedListener_impl(l); + } + + /** + * Registers a callback to be invoked when the fullscreen mode should be changed. + */ + public void setFullScreenChangedListener(OnFullScreenChangedListener l) { + mProvider.setFullScreenChangedListener_impl(l); + } + + /** + * Interface definition of a callback to be invoked when the viw type has been changed. + */ + public interface OnViewTypeChangedListener { + /** + * Called when the view type has been changed. + * @see #setViewType(int) + * @param viewType + *
    + *
  • {@link #VIEW_TYPE_SURFACEVIEW} + *
  • {@link #VIEW_TYPE_TEXTUREVIEW} + *
+ */ + 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(); + } + + /** + * 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. + */ + public interface OnErrorListener { + // TODO: Redefine error codes. + /** + * 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 + */ + boolean onError(int what, int extra); + } + + /** + * Interface definition of a callback to be invoked to communicate some info and/or warning + * about the media or its playback. + */ + public interface OnInfoListener { + /** + * 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. + * + * @see MediaPlayer#OnInfoListener + */ + 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(); + } + + @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); + } + } +} -- cgit v1.2.3