Merge "[WebView Support Library] Implement onSafeBrowsingHit" into pi-androidx-dev
diff --git a/car/res/values/integers.xml b/car/res/values/integers.xml
index 1acb572..dcae680 100644
--- a/car/res/values/integers.xml
+++ b/car/res/values/integers.xml
@@ -31,10 +31,6 @@
<!-- Slide Down Menu -->
<integer name="car_slide_down_menu_column_number">4</integer>
- <!-- The length limit of body text in a paged list item. String longer than this limit should be
- truncated. -->
- <integer name="car_list_item_text_length_limit">120</integer>
-
<!-- The length limit of text in a borderless button. String longer than this limit should be
truncated. -->
<integer name="car_borderless_button_text_length_limit">20</integer>
diff --git a/car/src/main/java/androidx/car/utils/CarUxRestrictionsUtils.java b/car/src/main/java/androidx/car/utils/CarUxRestrictionsUtils.java
index e06ba80..7fec3c0 100644
--- a/car/src/main/java/androidx/car/utils/CarUxRestrictionsUtils.java
+++ b/car/src/main/java/androidx/car/utils/CarUxRestrictionsUtils.java
@@ -20,7 +20,6 @@
import android.car.drivingstate.CarUxRestrictions;
import android.content.Context;
-import androidx.annotation.RestrictTo;
import android.text.InputFilter;
import android.widget.TextView;
@@ -28,7 +27,7 @@
import java.util.Arrays;
import java.util.List;
-import androidx.car.R;
+import androidx.annotation.RestrictTo;
/**
* Utility class that helps {@code View}s comply with {@link CarUxRestrictions}.
@@ -53,8 +52,7 @@
*/
public static void comply(Context context, CarUxRestrictions carUxRestrictions, TextView tv) {
if (sStringLengthFilter == null) {
- int lengthLimit = context.getResources().getInteger(
- R.integer.car_list_item_text_length_limit);
+ int lengthLimit = carUxRestrictions.getMaxRestrictedStringLength();
sStringLengthFilter = new InputFilter.LengthFilter(lengthLimit);
}
diff --git a/compat/res/values/dimens.xml b/compat/res/values/dimens.xml
index 41abbe6..9ece458 100644
--- a/compat/res/values/dimens.xml
+++ b/compat/res/values/dimens.xml
@@ -72,8 +72,8 @@
<dimen name="notification_right_side_padding_top">2dp</dimen>
<!-- the maximum width of the large icon, above which it will be downscaled -->
- <dimen name="notification_icon_max_width">320dp</dimen>
+ <dimen name="compat_notification_large_icon_max_width">320dp</dimen>
<!-- the maximum height of the large icon, above which it will be downscaled -->
- <dimen name="notification_icon_max_height">320dp</dimen>
+ <dimen name="compat_notification_large_icon_max_height">320dp</dimen>
</resources>
diff --git a/compat/src/main/java/androidx/core/app/NotificationCompat.java b/compat/src/main/java/androidx/core/app/NotificationCompat.java
index aba40d1..9e60574 100644
--- a/compat/src/main/java/androidx/core/app/NotificationCompat.java
+++ b/compat/src/main/java/androidx/core/app/NotificationCompat.java
@@ -977,8 +977,10 @@
}
Resources res = mContext.getResources();
- int maxWidth = res.getDimensionPixelSize(R.dimen.notification_icon_max_width);
- int maxHeight = res.getDimensionPixelSize(R.dimen.notification_icon_max_height);
+ int maxWidth =
+ res.getDimensionPixelSize(R.dimen.compat_notification_large_icon_max_width);
+ int maxHeight =
+ res.getDimensionPixelSize(R.dimen.compat_notification_large_icon_max_height);
if (icon.getWidth() <= maxWidth && icon.getHeight() <= maxHeight) {
return icon;
}
diff --git a/media-widget/OWNERS b/media-widget/OWNERS
new file mode 100644
index 0000000..c740b62
--- /dev/null
+++ b/media-widget/OWNERS
@@ -0,0 +1,6 @@
+sungsoo@google.com
+insun@google.com
+jinpark@google.com
+hdmoon@google.com
+jaewan@google.com
+akersten@google.com
\ No newline at end of file
diff --git a/media-widget/api/current.txt b/media-widget/api/current.txt
new file mode 100644
index 0000000..d531c3f
--- /dev/null
+++ b/media-widget/api/current.txt
@@ -0,0 +1,36 @@
+package androidx.media.widget {
+
+ public class MediaControlView2 extends android.view.ViewGroup {
+ ctor public MediaControlView2(android.content.Context);
+ ctor public MediaControlView2(android.content.Context, android.util.AttributeSet);
+ ctor public MediaControlView2(android.content.Context, android.util.AttributeSet, int);
+ ctor public MediaControlView2(android.content.Context, android.util.AttributeSet, int, int);
+ method public void onMeasure(int, int);
+ method public void requestPlayButtonFocus();
+ }
+
+ public class VideoView2 extends android.view.ViewGroup {
+ ctor public VideoView2(android.content.Context);
+ ctor public VideoView2(android.content.Context, android.util.AttributeSet);
+ ctor public VideoView2(android.content.Context, android.util.AttributeSet, int);
+ ctor public VideoView2(android.content.Context, android.util.AttributeSet, int, int);
+ method public androidx.media.widget.MediaControlView2 getMediaControlView2();
+ method public float getSpeed();
+ method public int getViewType();
+ method public boolean isSubtitleEnabled();
+ method public void onAttachedToWindow();
+ method public void onDetachedFromWindow();
+ method public void onMeasure(int, int);
+ method public void setAudioAttributes(android.media.AudioAttributes);
+ method public void setAudioFocusRequest(int);
+ method public void setMediaControlView2(androidx.media.widget.MediaControlView2, long);
+ method public void setSpeed(float);
+ method public void setSubtitleEnabled(boolean);
+ method public void setVideoUri(android.net.Uri, java.util.Map<java.lang.String, java.lang.String>);
+ method public void setViewType(int);
+ field public static final int VIEW_TYPE_SURFACEVIEW = 0; // 0x0
+ field public static final int VIEW_TYPE_TEXTUREVIEW = 1; // 0x1
+ }
+
+}
+
diff --git a/media-widget/build.gradle b/media-widget/build.gradle
new file mode 100644
index 0000000..e6ff65d
--- /dev/null
+++ b/media-widget/build.gradle
@@ -0,0 +1,49 @@
+/*
+ * 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.
+ */
+
+import static androidx.build.dependencies.DependenciesKt.*
+import androidx.build.LibraryGroups
+import androidx.build.LibraryVersions
+
+plugins {
+ id("SupportAndroidLibraryPlugin")
+}
+
+dependencies {
+ api(project(":media"))
+ api(project(":mediarouter"))
+ api(project(":appcompat"))
+ api(project(":palette"))
+
+ androidTestImplementation(TEST_RUNNER_TMP, libs.exclude_for_espresso)
+ androidTestImplementation(ESPRESSO_CORE_TMP, libs.exclude_for_espresso)
+ androidTestImplementation(TEST_RULES_TMP, libs.exclude_for_espresso)
+ androidTestImplementation(MOCKITO_CORE, libs.exclude_bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(DEXMAKER_MOCKITO, libs.exclude_bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation project(':internal-testutils')
+}
+
+supportLibrary {
+ name = "Android Support Media Widget"
+ publish = true
+ mavenVersion = LibraryVersions.SUPPORT_LIBRARY
+ mavenGroup = LibraryGroups.MEDIA
+ inceptionYear = "2011"
+ description = "Android Support Media Widget"
+ minSdkVersion = 19
+ failOnDeprecationWarnings = false
+ failOnDeprecationWarnings = false
+}
diff --git a/media-widget/src/androidTest/AndroidManifest.xml b/media-widget/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..dd6e9e0
--- /dev/null
+++ b/media-widget/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="androidx.media.widget.test">
+ <uses-sdk android:targetSdkVersion="${target-sdk-version}"/>
+
+ <application>
+ <activity android:name="androidx.media.widget.VideoView2TestActivity"
+ android:configChanges="keyboardHidden|orientation|screenSize"
+ android:label="VideoView2TestActivity">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+ </intent-filter>
+ </activity>
+ </application>
+
+</manifest>
diff --git a/media/src/androidTest/java/androidx/widget/MediaUtils2.java b/media-widget/src/androidTest/java/androidx/media/widget/MediaUtils2.java
similarity index 98%
rename from media/src/androidTest/java/androidx/widget/MediaUtils2.java
rename to media-widget/src/androidTest/java/androidx/media/widget/MediaUtils2.java
index 29eb9f6..59b2567 100644
--- a/media/src/androidTest/java/androidx/widget/MediaUtils2.java
+++ b/media-widget/src/androidTest/java/androidx/media/widget/MediaUtils2.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package androidx.widget;
+package androidx.media.widget;
import android.content.Context;
import android.content.res.AssetFileDescriptor;
diff --git a/media/src/androidTest/java/androidx/widget/VideoView2Test.java b/media-widget/src/androidTest/java/androidx/media/widget/VideoView2Test.java
similarity index 98%
rename from media/src/androidTest/java/androidx/widget/VideoView2Test.java
rename to media-widget/src/androidTest/java/androidx/media/widget/VideoView2Test.java
index 7f3dcfc..2876e47 100644
--- a/media/src/androidTest/java/androidx/widget/VideoView2Test.java
+++ b/media-widget/src/androidTest/java/androidx/media/widget/VideoView2Test.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.widget;
+package androidx.media.widget;
import static android.content.Context.KEYGUARD_SERVICE;
@@ -44,7 +44,7 @@
import android.view.WindowManager;
import androidx.media.AudioAttributesCompat;
-import androidx.media.test.R;
+import androidx.media.widget.test.R;
import org.junit.After;
import org.junit.Before;
diff --git a/media/src/androidTest/java/androidx/widget/VideoView2TestActivity.java b/media-widget/src/androidTest/java/androidx/media/widget/VideoView2TestActivity.java
similarity index 93%
rename from media/src/androidTest/java/androidx/widget/VideoView2TestActivity.java
rename to media-widget/src/androidTest/java/androidx/media/widget/VideoView2TestActivity.java
index 26a9c92..d6a3ebc 100644
--- a/media/src/androidTest/java/androidx/widget/VideoView2TestActivity.java
+++ b/media-widget/src/androidTest/java/androidx/media/widget/VideoView2TestActivity.java
@@ -14,12 +14,12 @@
* limitations under the License.
*/
-package androidx.widget;
+package androidx.media.widget;
import android.app.Activity;
import android.os.Bundle;
-import androidx.media.test.R;
+import androidx.media.widget.test.R;
/**
* A minimal application for {@link VideoView2} test.
diff --git a/media-widget/src/androidTest/res/layout/videoview2_layout.xml b/media-widget/src/androidTest/res/layout/videoview2_layout.xml
new file mode 100644
index 0000000..40e07d5
--- /dev/null
+++ b/media-widget/src/androidTest/res/layout/videoview2_layout.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <androidx.media.widget.VideoView2
+ android:id="@+id/videoview"
+ android:layout_width="160dp"
+ android:layout_height="120dp"/>
+</LinearLayout>
diff --git a/media-widget/src/androidTest/res/raw/testvideo.3gp b/media-widget/src/androidTest/res/raw/testvideo.3gp
new file mode 100644
index 0000000..8329311
--- /dev/null
+++ b/media-widget/src/androidTest/res/raw/testvideo.3gp
Binary files differ
diff --git a/media-widget/src/androidTest/res/values/themes.xml b/media-widget/src/androidTest/res/values/themes.xml
new file mode 100644
index 0000000..2843f5e
--- /dev/null
+++ b/media-widget/src/androidTest/res/values/themes.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+
+<resources>
+
+ <style name="HasWindowTitle">
+ <item name="windowNoTitle">false</item>
+ </style>
+
+</resources>
\ No newline at end of file
diff --git a/media-widget/src/main/AndroidManifest.xml b/media-widget/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..824903e
--- /dev/null
+++ b/media-widget/src/main/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+<manifest package="androidx.media.widget"/>
diff --git a/media/src/main/java/androidx/widget/BaseLayout.java b/media-widget/src/main/java/androidx/media/widget/BaseLayout.java
similarity index 93%
rename from media/src/main/java/androidx/widget/BaseLayout.java
rename to media-widget/src/main/java/androidx/media/widget/BaseLayout.java
index 127006e..0b6988a 100644
--- a/media/src/main/java/androidx/widget/BaseLayout.java
+++ b/media-widget/src/main/java/androidx/media/widget/BaseLayout.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.widget;
+package androidx.media.widget;
import android.content.Context;
import android.graphics.drawable.Drawable;
@@ -22,8 +22,6 @@
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
-import android.view.ViewGroup.LayoutParams;
-import android.view.ViewGroup.MarginLayoutParams;
import androidx.annotation.AttrRes;
import androidx.annotation.NonNull;
@@ -83,8 +81,8 @@
int count = getChildCount();
final boolean measureMatchParentChildren =
- View.MeasureSpec.getMode(widthMeasureSpec) != View.MeasureSpec.EXACTLY
- || View.MeasureSpec.getMode(heightMeasureSpec) != View.MeasureSpec.EXACTLY;
+ MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY
+ || MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
mMatchParentChildren.clear();
int maxHeight = 0;
@@ -144,8 +142,8 @@
final int width = Math.max(0, getMeasuredWidth()
- getPaddingLeftWithForeground() - getPaddingRightWithForeground()
- lp.leftMargin - lp.rightMargin);
- childWidthMeasureSpec = View.MeasureSpec.makeMeasureSpec(
- width, View.MeasureSpec.EXACTLY);
+ childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
+ width, MeasureSpec.EXACTLY);
} else {
childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
getPaddingLeftWithForeground() + getPaddingRightWithForeground()
@@ -157,8 +155,8 @@
final int height = Math.max(0, getMeasuredHeight()
- getPaddingTopWithForeground() - getPaddingBottomWithForeground()
- lp.topMargin - lp.bottomMargin);
- childHeightMeasureSpec = View.MeasureSpec.makeMeasureSpec(
- height, View.MeasureSpec.EXACTLY);
+ childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
+ height, MeasureSpec.EXACTLY);
} else {
childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
getPaddingTopWithForeground() + getPaddingBottomWithForeground()
diff --git a/media/src/main/java/androidx/widget/MediaControlView2.java b/media-widget/src/main/java/androidx/media/widget/MediaControlView2.java
similarity index 91%
rename from media/src/main/java/androidx/widget/MediaControlView2.java
rename to media-widget/src/main/java/androidx/media/widget/MediaControlView2.java
index 51d427f..81085c3 100644
--- a/media/src/main/java/androidx/widget/MediaControlView2.java
+++ b/media-widget/src/main/java/androidx/media/widget/MediaControlView2.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2017 The Android Open Source Project
+ * 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.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.widget;
+package androidx.media.widget;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
@@ -27,6 +27,7 @@
import android.support.v4.media.session.PlaybackStateCompat;
import android.util.AttributeSet;
import android.view.Gravity;
+import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
@@ -50,11 +51,7 @@
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
-import androidx.media.R;
import androidx.media.SessionToken2;
-// import androidx.mediarouter.app.MediaRouteButton;
-// import androidx.mediarouter.media.MediaRouter;
-// import androidx.mediarouter.media.MediaRouteSelector;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -65,38 +62,37 @@
import java.util.Locale;
/**
- * @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.
+ * A View that contains the controls for {@link android.media.MediaPlayer}.
+ * It provides a wide range of buttons that serve the following functions: play/pause,
+ * rewind/fast-forward, skip to next/previous, select subtitle track, enter/exit full screen mode,
+ * adjust video quality, select audio track, mute/unmute, and adjust playback speed.
*
* <p>
* <em> MediaControlView2 can be initialized in two different ways: </em>
- * 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.
+ * 1) When initializing {@link VideoView2} a default MediaControlView2 is created.
+ * 2) Initialize MediaControlView2 programmatically and add it to a {@link ViewGroup} instance.
*
* 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 MediaController instance and set it to MediaControlView2
- * by calling setController(MediaController controller).
+ * which is necessary to communicate with MediaSession. In the second option, however, the
+ * developer needs to manually retrieve a MediaController instance from MediaSession and set it to
+ * MediaControlView2.
*
* <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().
+ * one can directly change the visibility of this view by calling {@link View#setVisibility(int)}.
+ * The values supported are View.VISIBLE and View.GONE.
*
* <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.
+ * In addition, the following customizations are supported:
+ * 1) Set focus to the play/pause button by calling requestPlayButtonFocus().
+ * 2) Set full screen mode
+ *
*/
@RequiresApi(21) // TODO correct minSdk API use incompatibilities and remove before release.
-@RestrictTo(LIBRARY_GROUP)
public class MediaControlView2 extends BaseLayout {
- /** @hide */
+ /**
+ * @hide
+ */
@RestrictTo(LIBRARY_GROUP)
@IntDef({
BUTTON_PLAY_PAUSE,
@@ -185,8 +181,6 @@
static final String ARGUMENT_KEY_FULLSCREEN = "fullScreen";
- // TODO: Make these constants public api to support custom video view.
- // TODO: Combine these constants into one regarding TrackInfo.
static final String KEY_VIDEO_TRACK_COUNT = "VideoTrackCount";
static final String KEY_AUDIO_TRACK_COUNT = "AudioTrackCount";
static final String KEY_SUBTITLE_TRACK_COUNT = "SubtitleTrackCount";
@@ -194,8 +188,6 @@
static final String KEY_SELECTED_AUDIO_INDEX = "SelectedAudioIndex";
static final String KEY_SELECTED_SUBTITLE_INDEX = "SelectedSubtitleIndex";
static final String EVENT_UPDATE_TRACK_STATUS = "UpdateTrackStatus";
-
- // TODO: Remove this once integrating with MediaSession2 & MediaMetadata2
static final String KEY_STATE_IS_ADVERTISEMENT = "MediaTypeAdvertisement";
static final String EVENT_UPDATE_MEDIA_TYPE_STATUS = "UpdateMediaTypeStatus";
@@ -203,8 +195,8 @@
static final String COMMAND_SHOW_SUBTITLE = "showSubtitle";
// String for sending command to hide subtitle to MediaSession.
static final String COMMAND_HIDE_SUBTITLE = "hideSubtitle";
- // TODO: remove once the implementation is revised
- public static final String COMMAND_SET_FULLSCREEN = "setFullscreen";
+ // String for sending command to set fullscreen to MediaSession.
+ static final String COMMAND_SET_FULLSCREEN = "setFullscreen";
// String for sending command to select audio track to MediaSession.
static final String COMMAND_SELECT_AUDIO_TRACK = "SelectTrack";
// String for sending command to set playback speed to MediaSession.
@@ -228,7 +220,6 @@
private static final int SIZE_TYPE_EMBEDDED = 0;
private static final int SIZE_TYPE_FULL = 1;
- // TODO: add support for Minimal size type.
private static final int SIZE_TYPE_MINIMAL = 2;
private static final int MAX_PROGRESS = 1000;
@@ -282,9 +273,6 @@
private TextView mTitleView;
private View mAdExternalLink;
private ImageButton mBackButton;
- // TODO (b/77158231) revive
- // private MediaRouteButton mRouteButton;
- // private MediaRouteSelector mRouteSelector;
// Relating to Center View
private ViewGroup mCenterView;
@@ -357,12 +345,6 @@
public MediaControlView2(@NonNull Context context, @Nullable AttributeSet attrs,
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);
super(context, attrs, defStyleAttr, defStyleRes);
mResources = getContext().getResources();
@@ -373,27 +355,32 @@
/**
* Sets MediaSession2 token to control corresponding MediaSession2.
+ * @hide
*/
+ @RestrictTo(LIBRARY_GROUP)
public void setMediaSessionToken(SessionToken2 token) {
- //mProvider.setMediaSessionToken_impl(token);
}
/**
* Registers a callback to be invoked when the fullscreen mode should be changed.
* @param l The callback that will be run
+ * @hide
*/
+ @RestrictTo(LIBRARY_GROUP)
public void setOnFullScreenListener(OnFullScreenListener l) {
- //mProvider.setOnFullScreenListener_impl(l);
}
/**
+ * Sets MediaController instance to MediaControlView2, which makes it possible to send and
+ * receive data between MediaControlView2 and VideoView2. This method does not need to be called
+ * when MediaControlView2 is initialized with VideoView2.
* @hide TODO: remove once the implementation is revised
*/
@RestrictTo(LIBRARY_GROUP)
public void setController(MediaControllerCompat controller) {
mController = controller;
if (controller != null) {
- mControls = controller.getTransportControls();
+ mControls = mController.getTransportControls();
// Set mMetadata and mPlaybackState to existing MediaSession variables since they may
// be called before the callback is called
mPlaybackState = mController.getPlaybackState();
@@ -427,8 +414,6 @@
*/
@RestrictTo(LIBRARY_GROUP)
public void setButtonVisibility(@Button int button, /*@Visibility*/ int visibility) {
- // TODO: add member variables for Fast-Forward/Prvious/Rewind buttons to save visibility in
- // order to prevent being overriden inside updateLayout().
switch (button) {
case MediaControlView2.BUTTON_PLAY_PAUSE:
if (mPlayPauseButton != null && canPause()) {
@@ -497,7 +482,9 @@
/**
* Interface definition of a callback to be invoked to inform the fullscreen mode is changed.
* Application should handle the fullscreen mode accordingly.
+ * @hide
*/
+ @RestrictTo(LIBRARY_GROUP)
public interface OnFullScreenListener {
/**
* Called to indicate a fullscreen mode change.
@@ -515,7 +502,6 @@
return false;
}
- // TODO: Should this function be removed?
@Override
public boolean onTrackballEvent(MotionEvent ev) {
return false;
@@ -549,7 +535,6 @@
R.dimen.mcv2_embedded_icon_size);
int marginSize = mResources.getDimensionPixelSize(R.dimen.mcv2_icon_margin);
- // TODO: add support for Advertisement Mode.
if (mMediaType == MEDIA_TYPE_DEFAULT) {
// Max number of icons inside BottomBarRightView for Music mode is 4.
int maxIconCount = 4;
@@ -579,7 +564,6 @@
mPrevWidth = currWidth;
mPrevHeight = currHeight;
}
- // TODO: move this to a different location.
// Update title bar parameters in order to avoid overlap between title view and the right
// side of the title bar.
updateTitleBarLayout();
@@ -589,7 +573,6 @@
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
- // TODO: Merge the below code with disableUnsupportedButtons().
if (mPlayPauseButton != null) {
mPlayPauseButton.setEnabled(enabled);
}
@@ -624,20 +607,6 @@
}
}
- // TODO (b/77158231) revive once androidx.mediarouter.* packagaes are available.
- /*
- void setRouteSelector(MediaRouteSelector selector) {
- mRouteSelector = selector;
- if (mRouteSelector != null && !mRouteSelector.isEmpty()) {
- mRouteButton.setRouteSelector(selector, MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN);
- mRouteButton.setVisibility(View.VISIBLE);
- } else {
- mRouteButton.setRouteSelector(MediaRouteSelector.EMPTY);
- mRouteButton.setVisibility(View.GONE);
- }
- }
- */
-
///////////////////////////////////////////////////
// Protected or private methods
///////////////////////////////////////////////////
@@ -696,20 +665,19 @@
*
* @return The controller view.
*/
- // TODO: This was "protected". Determine if it should be protected in MCV2.
private ViewGroup makeControllerView() {
ViewGroup root = (ViewGroup) inflateLayout(getContext(), R.layout.media_controller);
initControllerView(root);
return root;
}
- // TODO(b/76444971) make sure this is compatible with ApiHelper's one in updatable.
private View inflateLayout(Context context, int resId) {
LayoutInflater inflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
return inflater.inflate(resId, null);
}
+ @SuppressWarnings("deprecation")
private void initControllerView(ViewGroup v) {
// Relating to Title Bar View
mTitleBar = v.findViewById(R.id.title_bar);
@@ -720,9 +688,6 @@
mBackButton.setOnClickListener(mBackListener);
mBackButton.setVisibility(View.GONE);
}
- // TODO (b/77158231) revive
- // mRouteButton = v.findViewById(R.id.cast);
-
// Relating to Center View
mCenterView = v.findViewById(R.id.center_view);
mTransportControls = inflateTransportControls(R.layout.embedded_transport_controls);
@@ -775,7 +740,6 @@
mFullScreenButton = v.findViewById(R.id.fullscreen);
if (mFullScreenButton != null) {
mFullScreenButton.setOnClickListener(mFullScreenListener);
- // TODO: Show Fullscreen button when only it is possible.
}
mOverflowButtonRight = v.findViewById(R.id.overflow_right);
if (mOverflowButtonRight != null) {
@@ -819,7 +783,7 @@
mSettingsWindowMargin = (-1) * mResources.getDimensionPixelSize(
R.dimen.mcv2_settings_offset);
mSettingsWindow = new PopupWindow(mSettingsListView, mEmbeddedSettingsItemWidth,
- ViewGroup.LayoutParams.WRAP_CONTENT, true);
+ LayoutParams.WRAP_CONTENT, true);
}
/**
@@ -837,13 +801,6 @@
if (mFfwdButton != null && !canSeekForward()) {
mFfwdButton.setEnabled(false);
}
- // TODO What we really should do is add a canSeek to the MediaPlayerControl interface;
- // this scheme can break the case when applications want to allow seek through the
- // progress bar but disable forward/backward buttons.
- //
- // However, currently the flags SEEK_BACKWARD_AVAILABLE, SEEK_FORWARD_AVAILABLE,
- // and SEEK_AVAILABLE are all (un)set together; as such the aforementioned issue
- // shouldn't arise in existing applications.
if (mProgress != null && !canSeekBackward() && !canSeekForward()) {
mProgress.setEnabled(false);
}
@@ -1033,14 +990,14 @@
}
};
- private final View.OnClickListener mPlayPauseListener = new View.OnClickListener() {
+ private final OnClickListener mPlayPauseListener = new OnClickListener() {
@Override
public void onClick(View v) {
togglePausePlayState();
}
};
- private final View.OnClickListener mRewListener = new View.OnClickListener() {
+ private final OnClickListener mRewListener = new OnClickListener() {
@Override
public void onClick(View v) {
int pos = getCurrentPosition() - REWIND_TIME_MS;
@@ -1049,7 +1006,7 @@
}
};
- private final View.OnClickListener mFfwdListener = new View.OnClickListener() {
+ private final OnClickListener mFfwdListener = new OnClickListener() {
@Override
public void onClick(View v) {
int pos = getCurrentPosition() + FORWARD_TIME_MS;
@@ -1058,28 +1015,32 @@
}
};
- private final View.OnClickListener mNextListener = new View.OnClickListener() {
+ private final OnClickListener mNextListener = new OnClickListener() {
@Override
public void onClick(View v) {
mControls.skipToNext();
}
};
- private final View.OnClickListener mPrevListener = new View.OnClickListener() {
+ private final OnClickListener mPrevListener = new OnClickListener() {
@Override
public void onClick(View v) {
mControls.skipToPrevious();
}
};
- private final View.OnClickListener mBackListener = new View.OnClickListener() {
+ private final OnClickListener mBackListener = new OnClickListener() {
@Override
public void onClick(View v) {
- // TODO: implement
+ View parent = (View) getParent();
+ if (parent != null) {
+ parent.onKeyDown(KeyEvent.KEYCODE_BACK,
+ new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK));
+ }
}
};
- private final View.OnClickListener mSubtitleListener = new View.OnClickListener() {
+ private final OnClickListener mSubtitleListener = new OnClickListener() {
@Override
public void onClick(View v) {
mSettingsMode = SETTINGS_MODE_SUBTITLE_TRACK;
@@ -1089,7 +1050,7 @@
}
};
- private final View.OnClickListener mVideoQualityListener = new View.OnClickListener() {
+ private final OnClickListener mVideoQualityListener = new OnClickListener() {
@Override
public void onClick(View v) {
mSettingsMode = SETTINGS_MODE_VIDEO_QUALITY;
@@ -1099,11 +1060,10 @@
}
};
- private final View.OnClickListener mFullScreenListener = new View.OnClickListener() {
+ private final OnClickListener mFullScreenListener = new OnClickListener() {
@Override
public void onClick(View v) {
final boolean isEnteringFullScreen = !mIsFullScreen;
- // TODO: Re-arrange the button layouts according to the UX.
if (isEnteringFullScreen) {
mFullScreenButton.setImageDrawable(
mResources.getDrawable(R.drawable.ic_fullscreen_exit, null));
@@ -1119,7 +1079,7 @@
}
};
- private final View.OnClickListener mOverflowRightListener = new View.OnClickListener() {
+ private final OnClickListener mOverflowRightListener = new OnClickListener() {
@Override
public void onClick(View v) {
mBasicControls.setVisibility(View.GONE);
@@ -1127,7 +1087,7 @@
}
};
- private final View.OnClickListener mOverflowLeftListener = new View.OnClickListener() {
+ private final OnClickListener mOverflowLeftListener = new OnClickListener() {
@Override
public void onClick(View v) {
mBasicControls.setVisibility(View.VISIBLE);
@@ -1135,7 +1095,7 @@
}
};
- private final View.OnClickListener mMuteButtonListener = new View.OnClickListener() {
+ private final OnClickListener mMuteButtonListener = new OnClickListener() {
@Override
public void onClick(View v) {
if (!mIsMute) {
@@ -1156,7 +1116,7 @@
}
};
- private final View.OnClickListener mSettingsButtonListener = new View.OnClickListener() {
+ private final OnClickListener mSettingsButtonListener = new OnClickListener() {
@Override
public void onClick(View v) {
mSettingsMode = SETTINGS_MODE_MAIN;
@@ -1180,7 +1140,6 @@
mSubSettingsAdapter.setCheckPosition(mSelectedSpeedIndex);
mSettingsMode = SETTINGS_MODE_PLAYBACK_SPEED;
} else if (position == SETTINGS_MODE_HELP) {
- // TODO: implement this.
mSettingsWindow.dismiss();
return;
}
@@ -1211,7 +1170,6 @@
mSettingsWindow.dismiss();
break;
case SETTINGS_MODE_HELP:
- // TODO: implement this.
break;
case SETTINGS_MODE_SUBTITLE_TRACK:
if (position != mSelectedSubtitleTrackIndex) {
@@ -1237,7 +1195,6 @@
mSettingsWindow.dismiss();
break;
case SETTINGS_MODE_VIDEO_QUALITY:
- // TODO: add support for video quality
mSelectedVideoQualityIndex = position;
mSettingsWindow.dismiss();
break;
@@ -1358,10 +1315,6 @@
int embeddedWidth = mTimeView.getWidth() + embeddedBottomBarRightWidthMax;
int screenMaxLength = Math.max(screenWidth, screenHeight);
- if (fullWidth > screenMaxLength) {
- // TODO: screen may be smaller than the length needed for Full size.
- }
-
boolean isFullSize = (mMediaType == MEDIA_TYPE_DEFAULT) ? (currWidth == screenMaxLength) :
(currWidth == screenWidth && currHeight == screenHeight);
@@ -1389,6 +1342,7 @@
}
}
+ @SuppressWarnings("deprecation")
private void updateLayoutForSizeChange(int sizeType) {
mSizeType = sizeType;
RelativeLayout.LayoutParams timeViewParams =
@@ -1519,7 +1473,6 @@
mRewButton.setVisibility(View.GONE);
}
}
- // TODO: Add support for Next and Previous buttons
mNextButton = v.findViewById(R.id.next);
if (mNextButton != null) {
mNextButton.setOnClickListener(mNextListener);
@@ -1594,7 +1547,6 @@
mSettingsWindowMargin - totalHeight, Gravity.BOTTOM | Gravity.RIGHT);
}
- @RequiresApi(26) // TODO correct minSdk API use incompatibilities and remove before release.
private class MediaControllerCallback extends MediaControllerCompat.Callback {
@Override
public void onPlaybackStateChanged(PlaybackStateCompat state) {
@@ -1666,16 +1618,12 @@
for (final PlaybackStateCompat.CustomAction action : customActions) {
ImageButton button = new ImageButton(getContext(),
null /* AttributeSet */, 0 /* Style */);
- // TODO: Apply R.style.BottomBarButton to this button using library context.
// Refer Constructor with argument (int defStyleRes) of View.java
button.setImageResource(action.getIcon());
- button.setTooltipText(action.getName());
final String actionString = action.getAction().toString();
- button.setOnClickListener(new View.OnClickListener() {
+ button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
- // TODO: Currently, we are just sending extras that came from session.
- // Is it the right behavior?
mControls.sendCustomAction(actionString, action.getExtras());
setVisibility(View.VISIBLE);
}
@@ -1704,7 +1652,6 @@
mAudioTrackCount = extras.getInt(KEY_AUDIO_TRACK_COUNT);
mAudioTrackList = new ArrayList<String>();
if (mAudioTrackCount > 0) {
- // TODO: add more text about track info.
for (int i = 0; i < mAudioTrackCount; i++) {
String track = mResources.getString(
R.string.MediaControlView2_audio_track_number_text, i + 1);
@@ -1783,14 +1730,12 @@
@Override
public long getItemId(int position) {
// Auto-generated method stub--does not have any purpose here
- // TODO: implement this.
return 0;
}
@Override
public Object getItem(int position) {
// Auto-generated method stub--does not have any purpose here
- // TODO: implement this.
return null;
}
@@ -1834,7 +1779,6 @@
}
}
- // TODO: extend this class from SettingsAdapter
private class SubSettingsAdapter extends BaseAdapter {
private List<String> mTexts;
private int mCheckPosition;
@@ -1861,14 +1805,12 @@
@Override
public long getItemId(int position) {
// Auto-generated method stub--does not have any purpose here
- // TODO: implement this.
return 0;
}
@Override
public Object getItem(int position) {
// Auto-generated method stub--does not have any purpose here
- // TODO: implement this.
return null;
}
diff --git a/media-widget/src/main/java/androidx/media/widget/SubtitleView.java b/media-widget/src/main/java/androidx/media/widget/SubtitleView.java
new file mode 100644
index 0000000..1faff2f
--- /dev/null
+++ b/media-widget/src/main/java/androidx/media/widget/SubtitleView.java
@@ -0,0 +1,144 @@
+/*
+ * 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 androidx.media.widget;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.os.Looper;
+import android.util.AttributeSet;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.media.subtitle.SubtitleController.Anchor;
+import androidx.media.subtitle.SubtitleTrack.RenderingWidget;
+
+@RequiresApi(21)
+class SubtitleView extends BaseLayout implements Anchor {
+ private static final String TAG = "SubtitleView";
+
+ private RenderingWidget mSubtitleWidget;
+ private RenderingWidget.OnChangedListener mSubtitlesChangedListener;
+
+ SubtitleView(Context context) {
+ this(context, null);
+ }
+
+ SubtitleView(Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ SubtitleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ SubtitleView(
+ Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ public void setSubtitleWidget(RenderingWidget subtitleWidget) {
+ if (mSubtitleWidget == subtitleWidget) {
+ return;
+ }
+
+ final boolean attachedToWindow = isAttachedToWindow();
+ if (mSubtitleWidget != null) {
+ if (attachedToWindow) {
+ mSubtitleWidget.onDetachedFromWindow();
+ }
+
+ mSubtitleWidget.setOnChangedListener(null);
+ }
+ mSubtitleWidget = subtitleWidget;
+
+ if (subtitleWidget != null) {
+ if (mSubtitlesChangedListener == null) {
+ mSubtitlesChangedListener = new RenderingWidget.OnChangedListener() {
+ @Override
+ public void onChanged(RenderingWidget renderingWidget) {
+ invalidate();
+ }
+ };
+ }
+
+ setWillNotDraw(false);
+ subtitleWidget.setOnChangedListener(mSubtitlesChangedListener);
+
+ if (attachedToWindow) {
+ subtitleWidget.onAttachedToWindow();
+ requestLayout();
+ }
+ } else {
+ setWillNotDraw(true);
+ }
+
+ invalidate();
+ }
+
+ @Override
+ public Looper getSubtitleLooper() {
+ return Looper.getMainLooper();
+ }
+
+ @Override
+ public void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ if (mSubtitleWidget != null) {
+ mSubtitleWidget.onAttachedToWindow();
+ }
+ }
+
+ @Override
+ public void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+
+ if (mSubtitleWidget != null) {
+ mSubtitleWidget.onDetachedFromWindow();
+ }
+ }
+
+ @Override
+ public void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+
+ if (mSubtitleWidget != null) {
+ final int width = getWidth() - getPaddingLeft() - getPaddingRight();
+ final int height = getHeight() - getPaddingTop() - getPaddingBottom();
+
+ mSubtitleWidget.setSize(width, height);
+ }
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ super.draw(canvas);
+
+ if (mSubtitleWidget != null) {
+ final int saveCount = canvas.save();
+ canvas.translate(getPaddingLeft(), getPaddingTop());
+ mSubtitleWidget.draw(canvas);
+ canvas.restoreToCount(saveCount);
+ }
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return SubtitleView.class.getName();
+ }
+}
diff --git a/media/src/main/java/androidx/widget/VideoSurfaceView.java b/media-widget/src/main/java/androidx/media/widget/VideoSurfaceView.java
similarity index 97%
rename from media/src/main/java/androidx/widget/VideoSurfaceView.java
rename to media-widget/src/main/java/androidx/media/widget/VideoSurfaceView.java
index eafa6f3..4415ea9 100644
--- a/media/src/main/java/androidx/widget/VideoSurfaceView.java
+++ b/media-widget/src/main/java/androidx/media/widget/VideoSurfaceView.java
@@ -14,9 +14,9 @@
* limitations under the License.
*/
-package androidx.widget;
+package androidx.media.widget;
-import static androidx.widget.VideoView2.VIEW_TYPE_SURFACEVIEW;
+import static androidx.media.widget.VideoView2.VIEW_TYPE_SURFACEVIEW;
import android.content.Context;
import android.graphics.Rect;
@@ -30,7 +30,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
-@RequiresApi(21)
+@RequiresApi(28)
class VideoSurfaceView extends SurfaceView implements VideoViewInterface, SurfaceHolder.Callback {
private static final String TAG = "VideoSurfaceView";
private static final boolean DEBUG = true; // STOPSHIP: Log.isLoggable(TAG, Log.DEBUG);
diff --git a/media/src/main/java/androidx/widget/VideoTextureView.java b/media-widget/src/main/java/androidx/media/widget/VideoTextureView.java
similarity index 97%
rename from media/src/main/java/androidx/widget/VideoTextureView.java
rename to media-widget/src/main/java/androidx/media/widget/VideoTextureView.java
index 836fdc3..f9f8df8 100644
--- a/media/src/main/java/androidx/widget/VideoTextureView.java
+++ b/media-widget/src/main/java/androidx/media/widget/VideoTextureView.java
@@ -14,9 +14,9 @@
* limitations under the License.
*/
-package androidx.widget;
+package androidx.media.widget;
-import static androidx.widget.VideoView2.VIEW_TYPE_TEXTUREVIEW;
+import static androidx.media.widget.VideoView2.VIEW_TYPE_TEXTUREVIEW;
import android.content.Context;
import android.graphics.SurfaceTexture;
@@ -30,7 +30,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
-@RequiresApi(21)
+@RequiresApi(28)
class VideoTextureView extends TextureView
implements VideoViewInterface, TextureView.SurfaceTextureListener {
private static final String TAG = "VideoTextureView";
diff --git a/media/src/main/java/androidx/widget/VideoView2.java b/media-widget/src/main/java/androidx/media/widget/VideoView2.java
similarity index 82%
rename from media/src/main/java/androidx/widget/VideoView2.java
rename to media-widget/src/main/java/androidx/media/widget/VideoView2.java
index 3cb1717..e246c09 100644
--- a/media/src/main/java/androidx/widget/VideoView2.java
+++ b/media-widget/src/main/java/androidx/media/widget/VideoView2.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.widget;
+package androidx.media.widget;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
@@ -31,7 +31,9 @@
import android.media.AudioManager;
import android.media.MediaMetadataRetriever;
import android.media.MediaPlayer;
+import android.media.MediaPlayer.OnSubtitleDataListener;
import android.media.PlaybackParams;
+import android.media.SubtitleData;
import android.net.Uri;
import android.os.Bundle;
import android.os.ResultReceiver;
@@ -47,7 +49,6 @@
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
-import android.view.ViewGroup.LayoutParams;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityManager;
import android.widget.ImageView;
@@ -63,8 +64,10 @@
import androidx.media.DataSourceDesc;
import androidx.media.MediaItem2;
import androidx.media.MediaMetadata2;
-import androidx.media.R;
import androidx.media.SessionToken2;
+import androidx.media.subtitle.ClosedCaptionRenderer;
+import androidx.media.subtitle.SubtitleController;
+import androidx.media.subtitle.SubtitleTrack;
import androidx.palette.graphics.Palette;
import java.io.IOException;
@@ -75,11 +78,9 @@
import java.util.Map;
import java.util.concurrent.Executor;
-// TODO: Replace MediaSession wtih MediaSession2 once MediaSession2 is submitted.
/**
- * @hide
- * Displays a video file. VideoView2 class is a View class which is wrapping {@link MediaPlayer}
- * so that developers can easily implement a video rendering application.
+ * Displays a video file. VideoView2 class is a ViewGroup class which is wrapping
+ * {@link MediaPlayer} so that developers can easily implement a video rendering application.
*
* <p>
* <em> Data sources that VideoView2 supports : </em>
@@ -100,20 +101,12 @@
* VideoView2 covers and inherits the most of
* VideoView's functionalities. The main differences are
* <ul>
- * <li> VideoView2 inherits FrameLayout and renders videos using SurfaceView and TextureView
+ * <li> VideoView2 inherits ViewGroup and renders videos using SurfaceView and TextureView
* selectively while VideoView inherits SurfaceView class.
* <li> 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,
- * <pre>
- * <VideoView2
- * android:id="@+id/video_view"
- * xmlns:widget="http://schemas.android.com/apk/com.android.media.update"
- * widget:enableControlView="false" />
- * </pre>
- * 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 MediaPlayer while VideoView is integrated with MediaPlayer.
+ * attached to VideoView2 by default.
+ * <li> If a developer wants to attach a customed MediaControlView2,
+ * assign the customed media control widget using {@link #setMediaControlView2}.
* <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.
@@ -135,8 +128,7 @@
* and restore these on their own in {@link android.app.Activity#onSaveInstanceState} and
* {@link android.app.Activity#onRestoreInstanceState}.
*/
-@RequiresApi(21) // TODO correct minSdk API use incompatibilities and remove before release.
-@RestrictTo(LIBRARY_GROUP)
+@RequiresApi(28) // TODO correct minSdk API use incompatibilities and remove before release.
public class VideoView2 extends BaseLayout implements VideoViewInterface.SurfaceListener {
/** @hide */
@RestrictTo(LIBRARY_GROUP)
@@ -162,7 +154,7 @@
public static final int VIEW_TYPE_TEXTUREVIEW = 1;
private static final String TAG = "VideoView2";
- private static final boolean DEBUG = true; // STOPSHIP: Log.isLoggable(TAG, Log.DEBUG);
+ private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
private static final long DEFAULT_SHOW_CONTROLLER_INTERVAL_MS = 2000;
private static final int STATE_ERROR = -1;
@@ -178,7 +170,6 @@
private static final int SIZE_TYPE_EMBEDDED = 0;
private static final int SIZE_TYPE_FULL = 1;
- // TODO: add support for Minimal size type.
private static final int SIZE_TYPE_MINIMAL = 2;
private AccessibilityManager mAccessibilityManager;
@@ -187,9 +178,9 @@
private int mAudioFocusType = AudioManager.AUDIOFOCUS_GAIN; // legacy focus gain
private boolean mAudioFocused = false;
- private Pair<Executor, VideoView2.OnCustomActionListener> mCustomActionListenerRecord;
- private VideoView2.OnViewTypeChangedListener mViewTypeChangedListener;
- private VideoView2.OnFullScreenRequestListener mFullScreenRequestListener;
+ private Pair<Executor, OnCustomActionListener> mCustomActionListenerRecord;
+ private OnViewTypeChangedListener mViewTypeChangedListener;
+ private OnFullScreenRequestListener mFullScreenRequestListener;
private VideoViewInterface mCurrentView;
private VideoTextureView mTextureView;
@@ -206,7 +197,6 @@
private Bundle mMediaTypeData;
private String mTitle;
- // TODO: move music view inside SurfaceView/TextureView or implement VideoViewInterface.
private WindowManager mManager;
private Resources mResources;
private View mMusicView;
@@ -232,102 +222,24 @@
private ArrayList<Integer> mVideoTrackIndices;
private ArrayList<Integer> mAudioTrackIndices;
- // private ArrayList<Pair<Integer, SubtitleTrack>> mSubtitleTrackIndices;
- // private SubtitleController mSubtitleController;
+ private ArrayList<Pair<Integer, SubtitleTrack>> mSubtitleTrackIndices;
+ private SubtitleController mSubtitleController;
// selected video/audio/subtitle track index as MediaPlayer returns
private int mSelectedVideoTrackIndex;
private int mSelectedAudioTrackIndex;
private int mSelectedSubtitleTrackIndex;
- // private SubtitleView mSubtitleView;
+ private SubtitleView mSubtitleView;
private boolean mSubtitleEnabled;
private float mSpeed;
- // TODO: Remove mFallbackSpeed when integration with MediaPlayer's new setPlaybackParams().
- // Refer: https://docs.google.com/document/d/1nzAfns6i2hJ3RkaUre3QMT6wsDedJ5ONLiA_OOBFFX8/edit
private float mFallbackSpeed; // keep the original speed before 'pause' is called.
private float mVolumeLevelFloat;
private int mVolumeLevel;
private long mShowControllerIntervalMs;
- // private MediaRouter mMediaRouter;
- // private MediaRouteSelector mRouteSelector;
- // private MediaRouter.RouteInfo mRoute;
- // private RoutePlayer mRoutePlayer;
-
- // TODO (b/77158231)
- /*
- private final MediaRouter.Callback mRouterCallback = new MediaRouter.Callback() {
- @Override
- public void onRouteSelected(MediaRouter router, MediaRouter.RouteInfo route) {
- if (route.supportsControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)) {
- // Stop local playback (if necessary)
- resetPlayer();
- mRoute = route;
- mRoutePlayer = new RoutePlayer(getContext(), route);
- mRoutePlayer.setPlayerEventCallback(new RoutePlayer.PlayerEventCallback() {
- @Override
- public void onPlayerStateChanged(MediaItemStatus itemStatus) {
- PlaybackStateCompat.Builder psBuilder = new PlaybackStateCompat.Builder();
- psBuilder.setActions(RoutePlayer.PLAYBACK_ACTIONS);
- long position = itemStatus.getContentPosition();
- switch (itemStatus.getPlaybackState()) {
- case MediaItemStatus.PLAYBACK_STATE_PENDING:
- psBuilder.setState(PlaybackStateCompat.STATE_NONE, position, 0);
- mCurrentState = STATE_IDLE;
- break;
- case MediaItemStatus.PLAYBACK_STATE_PLAYING:
- psBuilder.setState(PlaybackStateCompat.STATE_PLAYING, position, 1);
- mCurrentState = STATE_PLAYING;
- break;
- case MediaItemStatus.PLAYBACK_STATE_PAUSED:
- psBuilder.setState(PlaybackStateCompat.STATE_PAUSED, position, 0);
- mCurrentState = STATE_PAUSED;
- break;
- case MediaItemStatus.PLAYBACK_STATE_BUFFERING:
- psBuilder.setState(
- PlaybackStateCompat.STATE_BUFFERING, position, 0);
- mCurrentState = STATE_PAUSED;
- break;
- case MediaItemStatus.PLAYBACK_STATE_FINISHED:
- psBuilder.setState(PlaybackStateCompat.STATE_STOPPED, position, 0);
- mCurrentState = STATE_PLAYBACK_COMPLETED;
- break;
- }
-
- PlaybackStateCompat pbState = psBuilder.build();
- mMediaSession.setPlaybackState(pbState);
-
- MediaMetadataCompat.Builder mmBuilder = new MediaMetadataCompat.Builder();
- mmBuilder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION,
- itemStatus.getContentDuration());
- mMediaSession.setMetadata(mmBuilder.build());
- }
- });
- // Start remote playback (if necessary)
- mRoutePlayer.openVideo(mDsd);
- }
- }
-
- @Override
- public void onRouteUnselected(MediaRouter router, MediaRouter.RouteInfo route, int reason) {
- if (mRoute != null && mRoutePlayer != null) {
- mRoutePlayer.release();
- mRoutePlayer = null;
- }
- if (mRoute == route) {
- mRoute = null;
- }
- if (reason != MediaRouter.UNSELECT_REASON_ROUTE_CHANGED) {
- // TODO: Resume local playback (if necessary)
- openVideo(mDsd);
- }
- }
- };
- */
-
public VideoView2(@NonNull Context context) {
this(context, null);
}
@@ -349,7 +261,6 @@
mSpeed = 1.0f;
mFallbackSpeed = mSpeed;
mSelectedSubtitleTrackIndex = INVALID_TRACK_INDEX;
- // TODO: add attributes to get this value.
mShowControllerIntervalMs = DEFAULT_SHOW_CONTROLLER_INTERVAL_MS;
mAccessibilityManager = (AccessibilityManager) context.getSystemService(
@@ -362,7 +273,6 @@
setFocusableInTouchMode(true);
requestFocus();
- // TODO: try to keep a single child at a time rather than always having both.
mTextureView = new VideoTextureView(getContext());
mSurfaceView = new VideoSurfaceView(getContext());
LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT,
@@ -375,10 +285,10 @@
addView(mTextureView);
addView(mSurfaceView);
- // mSubtitleView = new SubtitleView(getContext());
- // mSubtitleView.setLayoutParams(params);
- // mSubtitleView.setBackgroundColor(0);
- // addView(mSubtitleView);
+ mSubtitleView = new SubtitleView(getContext());
+ mSubtitleView.setLayoutParams(params);
+ mSubtitleView.setBackgroundColor(0);
+ addView(mSubtitleView);
boolean enableControlView = (attrs == null) || attrs.getAttributeBooleanValue(
"http://schemas.android.com/apk/res/android",
@@ -391,7 +301,6 @@
"http://schemas.android.com/apk/res/android",
"enableSubtitle", false);
- // TODO: Choose TextureView when SurfaceView cannot be created.
// Choose surface view by default
int viewType = (attrs == null) ? VideoView2.VIEW_TYPE_SURFACEVIEW
: attrs.getAttributeIntValue(
@@ -408,15 +317,6 @@
mSurfaceView.setVisibility(View.GONE);
mCurrentView = mTextureView;
}
-
- // TODO (b/77158231)
- /*
- MediaRouteSelector.Builder builder = new MediaRouteSelector.Builder();
- builder.addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
- builder.addControlCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO);
- builder.addControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO);
- mRouteSelector = builder.build();
- */
}
/**
@@ -429,10 +329,6 @@
public void setMediaControlView2(MediaControlView2 mediaControlView, long intervalMs) {
mMediaControlView = mediaControlView;
mShowControllerIntervalMs = intervalMs;
- // TODO: Call MediaControlView2.setRouteSelector only when cast availalbe.
- // TODO (b/77158231)
- // mMediaControlView.setRouteSelector(mRouteSelector);
-
if (isAttachedToWindow()) {
attachMediaControlView();
}
@@ -472,7 +368,7 @@
* 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}
+ * this view is attached to window. Please check {@link View#isAttachedToWindow}
* before calling this method.
*
* @throws IllegalStateException if interal MediaSession is not created yet.
@@ -487,13 +383,14 @@
}
/**
- * Returns {@link androidx.media.SessionToken2} so that developers create their own
+ * Returns {@link SessionToken2} so that developers create their own
* {@link androidx.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.
* @hide
*/
+ @RestrictTo(LIBRARY_GROUP)
public SessionToken2 getMediaSessionToken() {
//return mProvider.getMediaSessionToken_impl();
return null;
@@ -530,7 +427,6 @@
* 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) {
if (speed <= 0.0f) {
Log.e(TAG, "Unsupported speed (" + speed + ") is ignored.");
@@ -544,6 +440,16 @@
}
/**
+ * Returns playback speed.
+ *
+ * It returns the same value that has been set by {@link #setSpeed}, if it was available value.
+ * If {@link #setSpeed} has not been called before, then the normal speed 1.0f will be returned.
+ */
+ public float getSpeed() {
+ return mSpeed;
+ }
+
+ /**
* 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},
@@ -586,7 +492,7 @@
*
* @param path the path of the video.
*
- * @hide TODO remove
+ * @hide
*/
@RestrictTo(LIBRARY_GROUP)
public void setVideoPath(String path) {
@@ -598,7 +504,7 @@
*
* @param uri the URI of the video.
*
- * @hide TODO remove
+ * @hide
*/
@RestrictTo(LIBRARY_GROUP)
public void setVideoUri(Uri uri) {
@@ -614,11 +520,8 @@
* 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
*/
- @RestrictTo(LIBRARY_GROUP)
- public void setVideoUri(Uri uri, Map<String, String> headers) {
+ public void setVideoUri(Uri uri, @Nullable Map<String, String> headers) {
mSeekWhenPrepared = 0;
openVideo(uri, headers);
}
@@ -628,9 +531,11 @@
* object to VideoView2 is {@link #setDataSource}.
* @param mediaItem the MediaItem2 to play
* @see #setDataSource
+ *
+ * @hide
*/
+ @RestrictTo(LIBRARY_GROUP)
public void setMediaItem(@NonNull MediaItem2 mediaItem) {
- //mProvider.setMediaItem_impl(mediaItem);
}
/**
@@ -639,8 +544,8 @@
* @see #setMediaItem
* @hide
*/
+ @RestrictTo(LIBRARY_GROUP)
public void setDataSource(@NonNull DataSourceDesc dataSource) {
- //mProvider.setDataSource_impl(dataSource);
}
/**
@@ -689,7 +594,7 @@
* 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
+ * @hide
*/
@RestrictTo(LIBRARY_GROUP)
public void setCustomActions(List<PlaybackStateCompat.CustomAction> actionList,
@@ -717,7 +622,7 @@
/**
* Registers a callback to be invoked when the fullscreen mode should be changed.
* @param l The callback that will be run
- * @hide TODO remove
+ * @hide
*/
@RestrictTo(LIBRARY_GROUP)
public void setFullScreenRequestListener(OnFullScreenRequestListener l) {
@@ -733,12 +638,7 @@
mMediaSession.setCallback(new MediaSessionCallback());
mMediaSession.setActive(true);
mMediaController = mMediaSession.getController();
- // TODO (b/77158231)
- // mMediaRouter = MediaRouter.getInstance(getContext());
- // mMediaRouter.setMediaSession(mMediaSession);
- // mMediaRouter.addCallback(mRouteSelector, mRouterCallback);
attachMediaControlView();
- // TODO: remove this after moving MediaSession creating code inside initializing VideoView2
if (mCurrentState == STATE_PREPARED) {
extractTracks();
extractMetadata();
@@ -794,7 +694,6 @@
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
- // TODO: Test touch event handling logic thoroughly and simplify the logic.
return super.dispatchTouchEvent(ev);
}
@@ -894,7 +793,11 @@
// Implements VideoViewInterface.SurfaceListener
///////////////////////////////////////////////////
+ /**
+ * @hide
+ */
@Override
+ @RestrictTo(LIBRARY_GROUP)
public void onSurfaceCreated(View view, int width, int height) {
if (DEBUG) {
Log.d(TAG, "onSurfaceCreated(). mCurrentState=" + mCurrentState
@@ -906,7 +809,11 @@
}
}
+ /**
+ * @hide
+ */
@Override
+ @RestrictTo(LIBRARY_GROUP)
public void onSurfaceDestroyed(View view) {
if (DEBUG) {
Log.d(TAG, "onSurfaceDestroyed(). mCurrentState=" + mCurrentState
@@ -914,16 +821,23 @@
}
}
+ /**
+ * @hide
+ */
@Override
+ @RestrictTo(LIBRARY_GROUP)
public void onSurfaceChanged(View view, int width, int height) {
- // TODO: Do we need to call requestLayout here?
if (DEBUG) {
Log.d(TAG, "onSurfaceChanged(). width/height: " + width + "/" + height
+ ", " + view.toString());
}
}
+ /**
+ * @hide
+ */
@Override
+ @RestrictTo(LIBRARY_GROUP)
public void onSurfaceTakeOverDone(VideoViewInterface view) {
if (DEBUG) {
Log.d(TAG, "onSurfaceTakeOverDone(). Now current view is: " + view);
@@ -951,8 +865,6 @@
}
private boolean isInPlaybackState() {
- // TODO (b/77158231)
- // return (mMediaPlayer != null || mRoutePlayer != null)
return (mMediaPlayer != null)
&& mCurrentState != STATE_ERROR
&& mCurrentState != STATE_IDLE
@@ -960,8 +872,6 @@
}
private boolean needToStart() {
- // TODO (b/77158231)
- // return (mMediaPlayer != null || mRoutePlayer != null)
return (mMediaPlayer != null)
&& isAudioGranted()
&& isWaitingPlayback();
@@ -989,11 +899,6 @@
case AudioManager.AUDIOFOCUS_LOSS:
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
- // There is no way to distinguish pause() by transient
- // audio focus loss and by other explicit actions.
- // TODO: If we can distinguish those cases, change the code to resume when it
- // gains audio focus again for AUDIOFOCUS_LOSS_TRANSIENT and
- // AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK
mAudioFocused = false;
if (isInPlaybackState() && mMediaPlayer.isPlaying()) {
mMediaController.getTransportControls().pause();
@@ -1004,6 +909,7 @@
}
};
+ @SuppressWarnings("deprecation")
private void requestAudioFocus(int focusType) {
int result;
if (android.os.Build.VERSION.SDK_INT >= 26) {
@@ -1031,8 +937,6 @@
private void openVideo(Uri uri, Map<String, String> headers) {
resetPlayer();
if (isRemotePlayback()) {
- // TODO (b/77158231)
- // mRoutePlayer.openVideo(dsd);
return;
}
@@ -1044,10 +948,9 @@
mCurrentView.assignSurfaceToMediaPlayer(mMediaPlayer);
final Context context = getContext();
- // TODO: Add timely firing logic for more accurate sync between CC and video frame
- // mSubtitleController = new SubtitleController(context);
- // mSubtitleController.registerRenderer(new ClosedCaptionRenderer(context));
- // mSubtitleController.setAnchor((SubtitleController.Anchor) mSubtitleView);
+ mSubtitleController = new SubtitleController(context);
+ mSubtitleController.registerRenderer(new ClosedCaptionRenderer(context));
+ mSubtitleController.setAnchor((SubtitleController.Anchor) mSubtitleView);
mMediaPlayer.setOnPreparedListener(mPreparedListener);
mMediaPlayer.setOnVideoSizeChangedListener(mSizeChangedListener);
@@ -1060,7 +963,7 @@
mCurrentBufferPercentage = -1;
mMediaPlayer.setDataSource(getContext(), uri, headers);
mMediaPlayer.setAudioAttributes(mAudioAttributes);
- // mMediaPlayer.setOnSubtitleDataListener(mSubtitleListener);
+ mMediaPlayer.setOnSubtitleDataListener(mSubtitleListener);
// we don't set the target state here either, but preserve the
// target state that was there before.
mCurrentState = STATE_PREPARING;
@@ -1091,6 +994,7 @@
/*
* Reset the media player in any state
*/
+ @SuppressWarnings("deprecation")
private void resetPlayer() {
if (mMediaPlayer != null) {
mMediaPlayer.reset();
@@ -1110,38 +1014,6 @@
private void updatePlaybackState() {
if (mStateBuilder == null) {
- /*
- // Get the capabilities of the player for this stream
- mMetadata = mMediaPlayer.getMetadata(MediaPlayer.METADATA_ALL,
- MediaPlayer.BYPASS_METADATA_FILTER);
-
- // Add Play action as default
- long playbackActions = PlaybackStateCompat.ACTION_PLAY;
- if (mMetadata != null) {
- if (!mMetadata.has(Metadata.PAUSE_AVAILABLE)
- || mMetadata.getBoolean(Metadata.PAUSE_AVAILABLE)) {
- playbackActions |= PlaybackStateCompat.ACTION_PAUSE;
- }
- if (!mMetadata.has(Metadata.SEEK_BACKWARD_AVAILABLE)
- || mMetadata.getBoolean(Metadata.SEEK_BACKWARD_AVAILABLE)) {
- playbackActions |= PlaybackStateCompat.ACTION_REWIND;
- }
- if (!mMetadata.has(Metadata.SEEK_FORWARD_AVAILABLE)
- || mMetadata.getBoolean(Metadata.SEEK_FORWARD_AVAILABLE)) {
- playbackActions |= PlaybackStateCompat.ACTION_FAST_FORWARD;
- }
- if (!mMetadata.has(Metadata.SEEK_AVAILABLE)
- || mMetadata.getBoolean(Metadata.SEEK_AVAILABLE)) {
- playbackActions |= PlaybackStateCompat.ACTION_SEEK_TO;
- }
- } else {
- playbackActions |= (PlaybackStateCompat.ACTION_PAUSE
- | PlaybackStateCompat.ACTION_REWIND
- | PlaybackStateCompat.ACTION_FAST_FORWARD
- | PlaybackStateCompat.ACTION_SEEK_TO);
- }
- */
- // TODO determine the actionable list based the metadata info.
long playbackActions = PlaybackStateCompat.ACTION_PLAY
| PlaybackStateCompat.ACTION_PAUSE
| PlaybackStateCompat.ACTION_REWIND | PlaybackStateCompat.ACTION_FAST_FORWARD
@@ -1160,8 +1032,6 @@
if (mCurrentState != STATE_ERROR
&& mCurrentState != STATE_IDLE
&& mCurrentState != STATE_PREPARING) {
- // TODO: this should be replaced with MediaPlayer2.getBufferedPosition() once it is
- // implemented.
if (mCurrentBufferPercentage == -1) {
mStateBuilder.setBufferedPosition(-1);
} else {
@@ -1208,7 +1078,6 @@
};
private void showController() {
- // TODO: Decide what to show when the state is not in playback state
if (mMediaControlView == null || !isInPlaybackState()
|| (mIsMusicMediaType && mSizeType == SIZE_TYPE_FULL)) {
return;
@@ -1232,7 +1101,6 @@
private void applySpeed() {
if (android.os.Build.VERSION.SDK_INT < 23) {
- // TODO: MediaPlayer2 will cover this, or implement with SoundPool.
return;
}
PlaybackParams params = mMediaPlayer.getPlaybackParams().allowDefaults();
@@ -1243,11 +1111,6 @@
mFallbackSpeed = mSpeed;
} catch (IllegalArgumentException e) {
Log.e(TAG, "PlaybackParams has unsupported value: " + e);
- // TODO: should revise this part after integrating with MP2.
- // If mSpeed had an illegal value for speed rate, system will determine best
- // handling (see PlaybackParams.AUDIO_FALLBACK_MODE_DEFAULT).
- // Note: The pre-MP2 returns 0.0f when it is paused. In this case, VideoView2 will
- // use mFallbackSpeed instead.
float fallbackSpeed = mMediaPlayer.getPlaybackParams().allowDefaults().getSpeed();
if (fallbackSpeed > 0.0f) {
mFallbackSpeed = fallbackSpeed;
@@ -1270,10 +1133,8 @@
if (!isInPlaybackState()) {
return;
}
- /*
if (select) {
if (mSubtitleTrackIndices.size() > 0) {
- // TODO: make this selection dynamic
mSelectedSubtitleTrackIndex = mSubtitleTrackIndices.get(0).first;
mSubtitleController.selectTrack(mSubtitleTrackIndices.get(0).second);
mMediaPlayer.selectTrack(mSelectedSubtitleTrackIndex);
@@ -1286,31 +1147,25 @@
mSubtitleView.setVisibility(View.GONE);
}
}
- */
}
private void extractTracks() {
MediaPlayer.TrackInfo[] trackInfos = mMediaPlayer.getTrackInfo();
mVideoTrackIndices = new ArrayList<>();
mAudioTrackIndices = new ArrayList<>();
- /*
mSubtitleTrackIndices = new ArrayList<>();
mSubtitleController.reset();
- */
for (int i = 0; i < trackInfos.length; ++i) {
int trackType = trackInfos[i].getTrackType();
if (trackType == MediaPlayer.TrackInfo.MEDIA_TRACK_TYPE_VIDEO) {
mVideoTrackIndices.add(i);
} else if (trackType == MediaPlayer.TrackInfo.MEDIA_TRACK_TYPE_AUDIO) {
mAudioTrackIndices.add(i);
- /*
- } else if (trackType == MediaPlayer.TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE
- || trackType == MediaPlayer.TrackInfo.MEDIA_TRACK_TYPE_TIMEDTEXT) {
+ } else if (trackType == MediaPlayer.TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE) {
SubtitleTrack track = mSubtitleController.addTrack(trackInfos[i].getFormat());
if (track != null) {
mSubtitleTrackIndices.add(new Pair<>(i, track));
}
- */
}
}
// Select first tracks as default
@@ -1327,12 +1182,10 @@
Bundle data = new Bundle();
data.putInt(MediaControlView2.KEY_VIDEO_TRACK_COUNT, mVideoTrackIndices.size());
data.putInt(MediaControlView2.KEY_AUDIO_TRACK_COUNT, mAudioTrackIndices.size());
- /*
data.putInt(MediaControlView2.KEY_SUBTITLE_TRACK_COUNT, mSubtitleTrackIndices.size());
if (mSubtitleTrackIndices.size() > 0) {
selectOrDeselectSubtitle(mSubtitleEnabled);
}
- */
mMediaSession.sendSessionEvent(MediaControlView2.EVENT_UPDATE_TRACK_STATUS, data);
}
@@ -1352,6 +1205,7 @@
}
}
+ @SuppressWarnings("deprecation")
private void extractAudioMetadata() {
if (!mIsMusicMediaType) {
return;
@@ -1366,12 +1220,10 @@
Bitmap bitmap = BitmapFactory.decodeByteArray(album, 0, album.length);
mMusicAlbumDrawable = new BitmapDrawable(bitmap);
- // TODO: replace with visualizer
Palette.Builder builder = Palette.from(bitmap);
builder.generate(new Palette.PaletteAsyncListener() {
@Override
public void onGenerated(Palette palette) {
- // TODO: add dominant color for default album image.
mDominantColor = palette.getDominantColor(0);
if (mMusicView != null) {
mMusicView.setBackgroundColor(mDominantColor);
@@ -1445,7 +1297,6 @@
addView(mMusicView, 0);
}
- /*
OnSubtitleDataListener mSubtitleListener =
new OnSubtitleDataListener() {
@Override
@@ -1473,7 +1324,6 @@
}
}
};
- */
MediaPlayer.OnVideoSizeChangedListener mSizeChangedListener =
new MediaPlayer.OnVideoSizeChangedListener() {
@@ -1505,8 +1355,6 @@
// Create and set playback state for MediaControlView2
updatePlaybackState();
- // TODO: change this to send TrackInfos to MediaControlView2
- // TODO: create MediaSession when initializing VideoView2
if (mMediaSession != null) {
extractTracks();
}
@@ -1550,20 +1398,12 @@
// Get and set duration and title values as MediaMetadata for MediaControlView2
MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder();
- // TODO: Get title via other public APIs.
- /*
- if (mMetadata != null && mMetadata.has(Metadata.TITLE)) {
- mTitle = mMetadata.getString(Metadata.TITLE);
- }
- */
builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, mTitle);
builder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, mMediaPlayer.getDuration());
if (mMediaSession != null) {
mMediaSession.setMetadata(builder.build());
- // TODO: merge this code with the above code when integrating with
- // MediaSession2.
if (mNeedUpdateMediaType) {
mMediaSession.sendSessionEvent(
MediaControlView2.EVENT_UPDATE_MEDIA_TYPE_STATUS, mMediaTypeData);
@@ -1583,6 +1423,7 @@
MediaPlayer.OnCompletionListener mCompletionListener = new MediaPlayer.OnCompletionListener() {
@Override
+ @SuppressWarnings("deprecation")
public void onCompletion(MediaPlayer mp) {
mCurrentState = STATE_PLAYBACK_COMPLETED;
mTargetState = STATE_PLAYBACK_COMPLETED;
@@ -1633,12 +1474,9 @@
@Override
public void onCommand(String command, Bundle args, ResultReceiver receiver) {
if (isRemotePlayback()) {
- // TODO (b/77158231)
- // mRoutePlayer.onCommand(command, args, receiver);
} else {
switch (command) {
case MediaControlView2.COMMAND_SHOW_SUBTITLE:
- /*
int subtitleIndex = args.getInt(
MediaControlView2.KEY_SELECTED_SUBTITLE_INDEX,
INVALID_TRACK_INDEX);
@@ -1649,7 +1487,6 @@
setSubtitleEnabled(true);
}
}
- */
break;
case MediaControlView2.COMMAND_HIDE_SUBTITLE:
setSubtitleEnabled(false);
@@ -1711,8 +1548,6 @@
if ((isInPlaybackState() && mCurrentView.hasAvailableSurface()) || mIsMusicMediaType) {
if (isRemotePlayback()) {
- // TODO (b/77158231)
- // mRoutePlayer.onPlay();
} else {
applySpeed();
mMediaPlayer.start();
@@ -1733,8 +1568,6 @@
public void onPause() {
if (isInPlaybackState()) {
if (isRemotePlayback()) {
- // TODO (b/77158231)
- // mRoutePlayer.onPause();
mCurrentState = STATE_PAUSED;
} else if (mMediaPlayer.isPlaying()) {
mMediaPlayer.pause();
@@ -1754,10 +1587,7 @@
public void onSeekTo(long pos) {
if (isInPlaybackState()) {
if (isRemotePlayback()) {
- // TODO (b/77158231)
- // mRoutePlayer.onSeekTo(pos);
} else {
- // TODO Refactor VideoView2 with FooImplBase and FooImplApiXX.
if (android.os.Build.VERSION.SDK_INT < 26) {
mMediaPlayer.seekTo((int) pos);
} else {
@@ -1774,8 +1604,6 @@
@Override
public void onStop() {
if (isRemotePlayback()) {
- // TODO (b/77158231)
- // mRoutePlayer.onStop();
} else {
resetPlayer();
}
diff --git a/media/src/main/java/androidx/widget/VideoViewInterface.java b/media-widget/src/main/java/androidx/media/widget/VideoViewInterface.java
similarity index 98%
rename from media/src/main/java/androidx/widget/VideoViewInterface.java
rename to media-widget/src/main/java/androidx/media/widget/VideoViewInterface.java
index eca8c82..81b40a9 100644
--- a/media/src/main/java/androidx/widget/VideoViewInterface.java
+++ b/media-widget/src/main/java/androidx/media/widget/VideoViewInterface.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.widget;
+package androidx.media.widget;
import android.media.MediaPlayer;
import android.view.View;
diff --git a/media/src/main/res/drawable/custom_progress.xml b/media-widget/src/main/res/drawable/custom_progress.xml
similarity index 100%
rename from media/src/main/res/drawable/custom_progress.xml
rename to media-widget/src/main/res/drawable/custom_progress.xml
diff --git a/media/src/main/res/drawable/custom_progress_thumb.xml b/media-widget/src/main/res/drawable/custom_progress_thumb.xml
similarity index 100%
rename from media/src/main/res/drawable/custom_progress_thumb.xml
rename to media-widget/src/main/res/drawable/custom_progress_thumb.xml
diff --git a/media/src/main/res/drawable/ic_arrow_back.xml b/media-widget/src/main/res/drawable/ic_arrow_back.xml
similarity index 100%
rename from media/src/main/res/drawable/ic_arrow_back.xml
rename to media-widget/src/main/res/drawable/ic_arrow_back.xml
diff --git a/media/src/main/res/drawable/ic_aspect_ratio.xml b/media-widget/src/main/res/drawable/ic_aspect_ratio.xml
similarity index 100%
rename from media/src/main/res/drawable/ic_aspect_ratio.xml
rename to media-widget/src/main/res/drawable/ic_aspect_ratio.xml
diff --git a/media/src/main/res/drawable/ic_audiotrack.xml b/media-widget/src/main/res/drawable/ic_audiotrack.xml
similarity index 100%
rename from media/src/main/res/drawable/ic_audiotrack.xml
rename to media-widget/src/main/res/drawable/ic_audiotrack.xml
diff --git a/media/src/main/res/drawable/ic_check.xml b/media-widget/src/main/res/drawable/ic_check.xml
similarity index 100%
rename from media/src/main/res/drawable/ic_check.xml
rename to media-widget/src/main/res/drawable/ic_check.xml
diff --git a/media/src/main/res/drawable/ic_chevron_left.xml b/media-widget/src/main/res/drawable/ic_chevron_left.xml
similarity index 100%
rename from media/src/main/res/drawable/ic_chevron_left.xml
rename to media-widget/src/main/res/drawable/ic_chevron_left.xml
diff --git a/media/src/main/res/drawable/ic_chevron_right.xml b/media-widget/src/main/res/drawable/ic_chevron_right.xml
similarity index 100%
rename from media/src/main/res/drawable/ic_chevron_right.xml
rename to media-widget/src/main/res/drawable/ic_chevron_right.xml
diff --git a/media/src/main/res/drawable/ic_default_album_image.xml b/media-widget/src/main/res/drawable/ic_default_album_image.xml
similarity index 100%
rename from media/src/main/res/drawable/ic_default_album_image.xml
rename to media-widget/src/main/res/drawable/ic_default_album_image.xml
diff --git a/media/src/main/res/drawable/ic_forward_30.xml b/media-widget/src/main/res/drawable/ic_forward_30.xml
similarity index 100%
rename from media/src/main/res/drawable/ic_forward_30.xml
rename to media-widget/src/main/res/drawable/ic_forward_30.xml
diff --git a/media/src/main/res/drawable/ic_fullscreen.xml b/media-widget/src/main/res/drawable/ic_fullscreen.xml
similarity index 100%
rename from media/src/main/res/drawable/ic_fullscreen.xml
rename to media-widget/src/main/res/drawable/ic_fullscreen.xml
diff --git a/media/src/main/res/drawable/ic_fullscreen_exit.xml b/media-widget/src/main/res/drawable/ic_fullscreen_exit.xml
similarity index 100%
rename from media/src/main/res/drawable/ic_fullscreen_exit.xml
rename to media-widget/src/main/res/drawable/ic_fullscreen_exit.xml
diff --git a/media/src/main/res/drawable/ic_help.xml b/media-widget/src/main/res/drawable/ic_help.xml
similarity index 100%
rename from media/src/main/res/drawable/ic_help.xml
rename to media-widget/src/main/res/drawable/ic_help.xml
diff --git a/media/src/main/res/drawable/ic_high_quality.xml b/media-widget/src/main/res/drawable/ic_high_quality.xml
similarity index 100%
rename from media/src/main/res/drawable/ic_high_quality.xml
rename to media-widget/src/main/res/drawable/ic_high_quality.xml
diff --git a/media/src/main/res/drawable/ic_launch.xml b/media-widget/src/main/res/drawable/ic_launch.xml
similarity index 100%
rename from media/src/main/res/drawable/ic_launch.xml
rename to media-widget/src/main/res/drawable/ic_launch.xml
diff --git a/media/src/main/res/drawable/ic_mute.xml b/media-widget/src/main/res/drawable/ic_mute.xml
similarity index 100%
rename from media/src/main/res/drawable/ic_mute.xml
rename to media-widget/src/main/res/drawable/ic_mute.xml
diff --git a/media/src/main/res/drawable/ic_pause_circle_filled.xml b/media-widget/src/main/res/drawable/ic_pause_circle_filled.xml
similarity index 100%
rename from media/src/main/res/drawable/ic_pause_circle_filled.xml
rename to media-widget/src/main/res/drawable/ic_pause_circle_filled.xml
diff --git a/media/src/main/res/drawable/ic_play_circle_filled.xml b/media-widget/src/main/res/drawable/ic_play_circle_filled.xml
similarity index 100%
rename from media/src/main/res/drawable/ic_play_circle_filled.xml
rename to media-widget/src/main/res/drawable/ic_play_circle_filled.xml
diff --git a/media/src/main/res/drawable/ic_replay_circle_filled.xml b/media-widget/src/main/res/drawable/ic_replay_circle_filled.xml
similarity index 100%
rename from media/src/main/res/drawable/ic_replay_circle_filled.xml
rename to media-widget/src/main/res/drawable/ic_replay_circle_filled.xml
diff --git a/media/src/main/res/drawable/ic_rewind_10.xml b/media-widget/src/main/res/drawable/ic_rewind_10.xml
similarity index 100%
rename from media/src/main/res/drawable/ic_rewind_10.xml
rename to media-widget/src/main/res/drawable/ic_rewind_10.xml
diff --git a/media/src/main/res/drawable/ic_sd.xml b/media-widget/src/main/res/drawable/ic_sd.xml
similarity index 100%
rename from media/src/main/res/drawable/ic_sd.xml
rename to media-widget/src/main/res/drawable/ic_sd.xml
diff --git a/media/src/main/res/drawable/ic_settings.xml b/media-widget/src/main/res/drawable/ic_settings.xml
similarity index 100%
rename from media/src/main/res/drawable/ic_settings.xml
rename to media-widget/src/main/res/drawable/ic_settings.xml
diff --git a/media/src/main/res/drawable/ic_skip_next.xml b/media-widget/src/main/res/drawable/ic_skip_next.xml
similarity index 100%
rename from media/src/main/res/drawable/ic_skip_next.xml
rename to media-widget/src/main/res/drawable/ic_skip_next.xml
diff --git a/media/src/main/res/drawable/ic_skip_previous.xml b/media-widget/src/main/res/drawable/ic_skip_previous.xml
similarity index 100%
rename from media/src/main/res/drawable/ic_skip_previous.xml
rename to media-widget/src/main/res/drawable/ic_skip_previous.xml
diff --git a/media/src/main/res/drawable/ic_subtitle_off.xml b/media-widget/src/main/res/drawable/ic_subtitle_off.xml
similarity index 100%
rename from media/src/main/res/drawable/ic_subtitle_off.xml
rename to media-widget/src/main/res/drawable/ic_subtitle_off.xml
diff --git a/media/src/main/res/drawable/ic_subtitle_on.xml b/media-widget/src/main/res/drawable/ic_subtitle_on.xml
similarity index 100%
rename from media/src/main/res/drawable/ic_subtitle_on.xml
rename to media-widget/src/main/res/drawable/ic_subtitle_on.xml
diff --git a/media/src/main/res/drawable/ic_unmute.xml b/media-widget/src/main/res/drawable/ic_unmute.xml
similarity index 100%
rename from media/src/main/res/drawable/ic_unmute.xml
rename to media-widget/src/main/res/drawable/ic_unmute.xml
diff --git a/media/src/main/res/layout/embedded_music.xml b/media-widget/src/main/res/layout/embedded_music.xml
similarity index 100%
rename from media/src/main/res/layout/embedded_music.xml
rename to media-widget/src/main/res/layout/embedded_music.xml
diff --git a/media/src/main/res/layout/embedded_settings_list_item.xml b/media-widget/src/main/res/layout/embedded_settings_list_item.xml
similarity index 100%
rename from media/src/main/res/layout/embedded_settings_list_item.xml
rename to media-widget/src/main/res/layout/embedded_settings_list_item.xml
diff --git a/media/src/main/res/layout/embedded_sub_settings_list_item.xml b/media-widget/src/main/res/layout/embedded_sub_settings_list_item.xml
similarity index 100%
rename from media/src/main/res/layout/embedded_sub_settings_list_item.xml
rename to media-widget/src/main/res/layout/embedded_sub_settings_list_item.xml
diff --git a/media/src/main/res/layout/embedded_transport_controls.xml b/media-widget/src/main/res/layout/embedded_transport_controls.xml
similarity index 100%
rename from media/src/main/res/layout/embedded_transport_controls.xml
rename to media-widget/src/main/res/layout/embedded_transport_controls.xml
diff --git a/media/src/main/res/layout/full_landscape_music.xml b/media-widget/src/main/res/layout/full_landscape_music.xml
similarity index 100%
rename from media/src/main/res/layout/full_landscape_music.xml
rename to media-widget/src/main/res/layout/full_landscape_music.xml
diff --git a/media/src/main/res/layout/full_portrait_music.xml b/media-widget/src/main/res/layout/full_portrait_music.xml
similarity index 100%
rename from media/src/main/res/layout/full_portrait_music.xml
rename to media-widget/src/main/res/layout/full_portrait_music.xml
diff --git a/media/src/main/res/layout/full_settings_list_item.xml b/media-widget/src/main/res/layout/full_settings_list_item.xml
similarity index 100%
rename from media/src/main/res/layout/full_settings_list_item.xml
rename to media-widget/src/main/res/layout/full_settings_list_item.xml
diff --git a/media/src/main/res/layout/full_sub_settings_list_item.xml b/media-widget/src/main/res/layout/full_sub_settings_list_item.xml
similarity index 100%
rename from media/src/main/res/layout/full_sub_settings_list_item.xml
rename to media-widget/src/main/res/layout/full_sub_settings_list_item.xml
diff --git a/media/src/main/res/layout/full_transport_controls.xml b/media-widget/src/main/res/layout/full_transport_controls.xml
similarity index 100%
rename from media/src/main/res/layout/full_transport_controls.xml
rename to media-widget/src/main/res/layout/full_transport_controls.xml
diff --git a/media/src/main/res/layout/media_controller.xml b/media-widget/src/main/res/layout/media_controller.xml
similarity index 100%
rename from media/src/main/res/layout/media_controller.xml
rename to media-widget/src/main/res/layout/media_controller.xml
diff --git a/media/src/main/res/layout/minimal_transport_controls.xml b/media-widget/src/main/res/layout/minimal_transport_controls.xml
similarity index 100%
rename from media/src/main/res/layout/minimal_transport_controls.xml
rename to media-widget/src/main/res/layout/minimal_transport_controls.xml
diff --git a/media/src/main/res/layout/settings_list.xml b/media-widget/src/main/res/layout/settings_list.xml
similarity index 100%
rename from media/src/main/res/layout/settings_list.xml
rename to media-widget/src/main/res/layout/settings_list.xml
diff --git a/media/src/main/res/layout/title_bar_gradient.xml b/media-widget/src/main/res/layout/title_bar_gradient.xml
similarity index 100%
rename from media/src/main/res/layout/title_bar_gradient.xml
rename to media-widget/src/main/res/layout/title_bar_gradient.xml
diff --git a/media/src/main/res/values/arrays.xml b/media-widget/src/main/res/values/arrays.xml
similarity index 100%
rename from media/src/main/res/values/arrays.xml
rename to media-widget/src/main/res/values/arrays.xml
diff --git a/media-widget/src/main/res/values/colors.xml b/media-widget/src/main/res/values/colors.xml
new file mode 100644
index 0000000..3aff9da
--- /dev/null
+++ b/media-widget/src/main/res/values/colors.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+ -->
+<resources>
+
+ <!-- The color of the material notification background for media notifications when no custom
+ color is specified -->
+ <color name="notification_material_background_media_default_color">#ff424242</color>
+
+ <color name="gray">#808080</color>
+ <color name="white">#ffffff</color>
+ <color name="white_opacity_70">#B3ffffff</color>
+ <color name="black_opacity_70">#B3000000</color>
+ <color name="title_bar_gradient_start">#50000000</color>
+ <color name="title_bar_gradient_end">#00000000</color>
+ <color name="bottom_bar_background">#40202020</color>
+</resources>
\ No newline at end of file
diff --git a/media-widget/src/main/res/values/dimens.xml b/media-widget/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..796f345
--- /dev/null
+++ b/media-widget/src/main/res/values/dimens.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+ -->
+
+<resources>
+ <dimen name="mcv2_embedded_settings_width">150dp</dimen>
+ <dimen name="mcv2_embedded_settings_height">36dp</dimen>
+ <dimen name="mcv2_embedded_settings_icon_size">20dp</dimen>
+ <dimen name="mcv2_embedded_settings_text_height">18dp</dimen>
+ <dimen name="mcv2_embedded_settings_main_text_size">12sp</dimen>
+ <dimen name="mcv2_embedded_settings_sub_text_size">10sp</dimen>
+ <dimen name="mcv2_full_settings_width">225dp</dimen>
+ <dimen name="mcv2_full_settings_height">54dp</dimen>
+ <dimen name="mcv2_full_settings_icon_size">30dp</dimen>
+ <dimen name="mcv2_full_settings_text_height">27dp</dimen>
+ <dimen name="mcv2_full_settings_main_text_size">16sp</dimen>
+ <dimen name="mcv2_full_settings_sub_text_size">13sp</dimen>
+ <dimen name="mcv2_settings_offset">8dp</dimen>
+
+ <dimen name="mcv2_transport_controls_padding">4dp</dimen>
+ <dimen name="mcv2_pause_icon_size">36dp</dimen>
+ <dimen name="mcv2_full_icon_size">28dp</dimen>
+ <dimen name="mcv2_embedded_icon_size">24dp</dimen>
+ <dimen name="mcv2_minimal_icon_size">24dp</dimen>
+ <dimen name="mcv2_icon_margin">10dp</dimen>
+
+ <dimen name="mcv2_full_album_image_portrait_size">232dp</dimen>
+ <dimen name="mcv2_full_album_image_landscape_size">176dp</dimen>
+
+ <dimen name="mcv2_custom_progress_max_size">2dp</dimen>
+ <dimen name="mcv2_custom_progress_thumb_size">12dp</dimen>
+ <dimen name="mcv2_buffer_view_height">5dp</dimen>
+</resources>
diff --git a/media/src/main/res/values/strings.xml b/media-widget/src/main/res/values/strings.xml
similarity index 92%
rename from media/src/main/res/values/strings.xml
rename to media-widget/src/main/res/values/strings.xml
index 4f46ccf..10889df 100644
--- a/media/src/main/res/values/strings.xml
+++ b/media-widget/src/main/res/values/strings.xml
@@ -1,22 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- 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.
--->
+<!--
+ ~ 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.
+ -->
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-
<!-- Name for the default system route prior to Jellybean. [CHAR LIMIT=30] -->
<string name="mr_system_route_name">System</string>
@@ -48,7 +48,7 @@
<!-- Button to disconnect from a media route. [CHAR LIMIT=30] -->
<string name="mr_controller_disconnect">Disconnect</string>
- <!-- Button to stop playback and disconnect from a media route. [CHAR LIMIT=30] -->
+ <!-- Button to stop playback and disconnect from a media route. [CHAR LIMIT=32] -->
<string name="mr_controller_stop_casting">Stop casting</string>
<!-- Content description for accessibility (not shown on the screen): dialog close button. [CHAR LIMIT=NONE] -->
diff --git a/media/src/main/res/values/style.xml b/media-widget/src/main/res/values/styles.xml
similarity index 100%
rename from media/src/main/res/values/style.xml
rename to media-widget/src/main/res/values/styles.xml
diff --git a/media/build.gradle b/media/build.gradle
index e55188a..af0928b 100644
--- a/media/build.gradle
+++ b/media/build.gradle
@@ -9,7 +9,6 @@
dependencies {
api(project(":annotation"))
api(project(":core"))
- api(project(":palette"))
androidTestImplementation(TEST_RUNNER_TMP, libs.exclude_for_espresso)
androidTestImplementation(ESPRESSO_CORE_TMP, libs.exclude_for_espresso)
diff --git a/media/src/androidTest/AndroidManifest.xml b/media/src/androidTest/AndroidManifest.xml
index 4fd25ca..32987a5 100644
--- a/media/src/androidTest/AndroidManifest.xml
+++ b/media/src/androidTest/AndroidManifest.xml
@@ -19,14 +19,6 @@
<uses-sdk android:targetSdkVersion="${target-sdk-version}"/>
<application>
- <activity android:name="androidx.widget.VideoView2TestActivity"
- android:configChanges="keyboardHidden|orientation|screenSize"
- android:label="VideoVeiw2TestActivity">
- <intent-filter>
- <action android:name="android.intent.action.MAIN" />
- <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
- </intent-filter>
- </activity>
<activity android:name="androidx.media.MediaStubActivity"
android:label="MediaStubActivity"
android:screenOrientation="nosensor"
diff --git a/media/src/androidTest/res/layout/videoview2_layout.xml b/media/src/androidTest/res/layout/videoview2_layout.xml
deleted file mode 100644
index e3783c0..0000000
--- a/media/src/androidTest/res/layout/videoview2_layout.xml
+++ /dev/null
@@ -1,26 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- * 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.
- -->
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:orientation="vertical"
- android:layout_width="match_parent"
- android:layout_height="match_parent">
-
- <androidx.widget.VideoView2
- android:id="@+id/videoview"
- android:layout_width="160dp"
- android:layout_height="120dp"/>
-</LinearLayout>
diff --git a/media/src/main/java/androidx/media/MediaBrowserServiceCompat.java b/media/src/main/java/androidx/media/MediaBrowserServiceCompat.java
index 430913e..8f24837 100644
--- a/media/src/main/java/androidx/media/MediaBrowserServiceCompat.java
+++ b/media/src/main/java/androidx/media/MediaBrowserServiceCompat.java
@@ -1020,8 +1020,11 @@
* Note that we cannot simply override {@link Service#attachBaseContext(Context)} and hide it
* because lint checks considers the overriden method as the new public API that needs update
* of current.txt.
+ *
+ * @hide
*/
- void attachToBaseContext(Context base) {
+ @RestrictTo(LIBRARY)
+ public void attachToBaseContext(Context base) {
attachBaseContext(base);
}
diff --git a/media/src/main/java/androidx/media/MediaPlayer2.java b/media/src/main/java/androidx/media/MediaPlayer2.java
index 3802b9f..f1cd3b6 100644
--- a/media/src/main/java/androidx/media/MediaPlayer2.java
+++ b/media/src/main/java/androidx/media/MediaPlayer2.java
@@ -1524,16 +1524,6 @@
*/
public static final int CALL_COMPLETED_PREPARE = 6;
- /** The player just completed a call {@link #releaseDrm}.
- * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
- */
- public static final int CALL_COMPLETED_RELEASE_DRM = 12;
-
- /** The player just completed a call {@link #restoreDrmKeys}.
- * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
- */
- public static final int CALL_COMPLETED_RESTORE_DRM_KEYS = 13;
-
/** The player just completed a call {@link #seekTo}.
* @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
*/
@@ -1621,8 +1611,6 @@
CALL_COMPLETED_PAUSE,
CALL_COMPLETED_PLAY,
CALL_COMPLETED_PREPARE,
- CALL_COMPLETED_RELEASE_DRM,
- CALL_COMPLETED_RESTORE_DRM_KEYS,
CALL_COMPLETED_SEEK_TO,
CALL_COMPLETED_SELECT_TRACK,
CALL_COMPLETED_SET_AUDIO_ATTRIBUTES,
@@ -1673,12 +1661,6 @@
*/
public static final int CALL_STATUS_ERROR_IO = 4;
- /** Status code represents that DRM operation is called before preparing a DRM scheme through
- * {@link #prepareDrm}.
- * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
- */
- public static final int CALL_STATUS_NO_DRM_SCHEME = 5;
-
/**
* @hide
*/
@@ -1688,8 +1670,7 @@
CALL_STATUS_INVALID_OPERATION,
CALL_STATUS_BAD_VALUE,
CALL_STATUS_PERMISSION_DENIED,
- CALL_STATUS_ERROR_IO,
- CALL_STATUS_NO_DRM_SCHEME})
+ CALL_STATUS_ERROR_IO})
@Retention(RetentionPolicy.SOURCE)
@RestrictTo(LIBRARY_GROUP)
public @interface CallStatus {}
diff --git a/media/src/main/java/androidx/media/MediaPlayer2Impl.java b/media/src/main/java/androidx/media/MediaPlayer2Impl.java
index 50c490f..6127473 100644
--- a/media/src/main/java/androidx/media/MediaPlayer2Impl.java
+++ b/media/src/main/java/androidx/media/MediaPlayer2Impl.java
@@ -1367,16 +1367,11 @@
*/
@Override
public void releaseDrm() throws NoDrmSchemeException {
- addTask(new Task(CALL_COMPLETED_RELEASE_DRM, false) {
- @Override
- void process() throws NoDrmSchemeException {
- try {
- mPlayer.releaseDrm();
- } catch (MediaPlayer.NoDrmSchemeException e) {
- throw new NoDrmSchemeException(e.getMessage());
- }
- }
- });
+ try {
+ mPlayer.releaseDrm();
+ } catch (MediaPlayer.NoDrmSchemeException e) {
+ throw new NoDrmSchemeException(e.getMessage());
+ }
}
@@ -1470,16 +1465,11 @@
@Override
public void restoreDrmKeys(@NonNull final byte[] keySetId)
throws NoDrmSchemeException {
- addTask(new Task(CALL_COMPLETED_RESTORE_DRM_KEYS, false) {
- @Override
- void process() throws NoDrmSchemeException {
- try {
- mPlayer.restoreKeys(keySetId);
- } catch (MediaPlayer.NoDrmSchemeException e) {
- throw new NoDrmSchemeException(e.getMessage());
- }
- }
- });
+ try {
+ mPlayer.restoreKeys(keySetId);
+ } catch (MediaPlayer.NoDrmSchemeException e) {
+ throw new NoDrmSchemeException(e.getMessage());
+ }
}
@@ -2040,8 +2030,6 @@
status = CALL_STATUS_PERMISSION_DENIED;
} catch (IOException e) {
status = CALL_STATUS_ERROR_IO;
- } catch (NoDrmSchemeException e) {
- status = CALL_STATUS_NO_DRM_SCHEME;
} catch (Exception e) {
status = CALL_STATUS_ERROR_UNKNOWN;
}
diff --git a/media/src/main/java/androidx/media/subtitle/Cea608CCParser.java b/media/src/main/java/androidx/media/subtitle/Cea608CCParser.java
new file mode 100644
index 0000000..9205fba
--- /dev/null
+++ b/media/src/main/java/androidx/media/subtitle/Cea608CCParser.java
@@ -0,0 +1,987 @@
+/*
+ * 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 androidx.media.subtitle;
+
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
+import android.text.TextPaint;
+import android.text.style.CharacterStyle;
+import android.text.style.StyleSpan;
+import android.text.style.UnderlineSpan;
+import android.text.style.UpdateAppearance;
+import android.util.Log;
+import android.view.accessibility.CaptioningManager.CaptionStyle;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+/**
+ * CCParser processes CEA-608 closed caption data.
+ *
+ * It calls back into OnDisplayChangedListener upon
+ * display change with styled text for rendering.
+ *
+ */
+class Cea608CCParser {
+ public static final int MAX_ROWS = 15;
+ public static final int MAX_COLS = 32;
+
+ private static final String TAG = "Cea608CCParser";
+ private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+ private static final int INVALID = -1;
+
+ // EIA-CEA-608: Table 70 - Control Codes
+ private static final int RCL = 0x20;
+ private static final int BS = 0x21;
+ private static final int AOF = 0x22;
+ private static final int AON = 0x23;
+ private static final int DER = 0x24;
+ private static final int RU2 = 0x25;
+ private static final int RU3 = 0x26;
+ private static final int RU4 = 0x27;
+ private static final int FON = 0x28;
+ private static final int RDC = 0x29;
+ private static final int TR = 0x2a;
+ private static final int RTD = 0x2b;
+ private static final int EDM = 0x2c;
+ private static final int CR = 0x2d;
+ private static final int ENM = 0x2e;
+ private static final int EOC = 0x2f;
+
+ // Transparent Space
+ private static final char TS = '\u00A0';
+
+ // Captioning Modes
+ private static final int MODE_UNKNOWN = 0;
+ private static final int MODE_PAINT_ON = 1;
+ private static final int MODE_ROLL_UP = 2;
+ private static final int MODE_POP_ON = 3;
+ private static final int MODE_TEXT = 4;
+
+ private final DisplayListener mListener;
+
+ private int mMode = MODE_PAINT_ON;
+ private int mRollUpSize = 4;
+ private int mPrevCtrlCode = INVALID;
+
+ private CCMemory mDisplay = new CCMemory();
+ private CCMemory mNonDisplay = new CCMemory();
+ private CCMemory mTextMem = new CCMemory();
+
+ Cea608CCParser(DisplayListener listener) {
+ mListener = listener;
+ }
+
+ public void parse(byte[] data) {
+ CCData[] ccData = CCData.fromByteArray(data);
+
+ for (int i = 0; i < ccData.length; i++) {
+ if (DEBUG) {
+ Log.d(TAG, ccData[i].toString());
+ }
+
+ if (handleCtrlCode(ccData[i])
+ || handleTabOffsets(ccData[i])
+ || handlePACCode(ccData[i])
+ || handleMidRowCode(ccData[i])) {
+ continue;
+ }
+
+ handleDisplayableChars(ccData[i]);
+ }
+ }
+
+ interface DisplayListener {
+ void onDisplayChanged(SpannableStringBuilder[] styledTexts);
+ CaptionStyle getCaptionStyle();
+ }
+
+ private CCMemory getMemory() {
+ // get the CC memory to operate on for current mode
+ switch (mMode) {
+ case MODE_POP_ON:
+ return mNonDisplay;
+ case MODE_TEXT:
+ // TODO(chz): support only caption mode for now,
+ // in text mode, dump everything to text mem.
+ return mTextMem;
+ case MODE_PAINT_ON:
+ case MODE_ROLL_UP:
+ return mDisplay;
+ default:
+ Log.w(TAG, "unrecoginized mode: " + mMode);
+ }
+ return mDisplay;
+ }
+
+ private boolean handleDisplayableChars(CCData ccData) {
+ if (!ccData.isDisplayableChar()) {
+ return false;
+ }
+
+ // Extended char includes 1 automatic backspace
+ if (ccData.isExtendedChar()) {
+ getMemory().bs();
+ }
+
+ getMemory().writeText(ccData.getDisplayText());
+
+ if (mMode == MODE_PAINT_ON || mMode == MODE_ROLL_UP) {
+ updateDisplay();
+ }
+
+ return true;
+ }
+
+ private boolean handleMidRowCode(CCData ccData) {
+ StyleCode m = ccData.getMidRow();
+ if (m != null) {
+ getMemory().writeMidRowCode(m);
+ return true;
+ }
+ return false;
+ }
+
+ private boolean handlePACCode(CCData ccData) {
+ PAC pac = ccData.getPAC();
+
+ if (pac != null) {
+ if (mMode == MODE_ROLL_UP) {
+ getMemory().moveBaselineTo(pac.getRow(), mRollUpSize);
+ }
+ getMemory().writePAC(pac);
+ return true;
+ }
+
+ return false;
+ }
+
+ private boolean handleTabOffsets(CCData ccData) {
+ int tabs = ccData.getTabOffset();
+
+ if (tabs > 0) {
+ getMemory().tab(tabs);
+ return true;
+ }
+
+ return false;
+ }
+
+ private boolean handleCtrlCode(CCData ccData) {
+ int ctrlCode = ccData.getCtrlCode();
+
+ if (mPrevCtrlCode != INVALID && mPrevCtrlCode == ctrlCode) {
+ // discard double ctrl codes (but if there's a 3rd one, we still take that)
+ mPrevCtrlCode = INVALID;
+ return true;
+ }
+
+ switch(ctrlCode) {
+ case RCL:
+ // select pop-on style
+ mMode = MODE_POP_ON;
+ break;
+ case BS:
+ getMemory().bs();
+ break;
+ case DER:
+ getMemory().der();
+ break;
+ case RU2:
+ case RU3:
+ case RU4:
+ mRollUpSize = (ctrlCode - 0x23);
+ // erase memory if currently in other style
+ if (mMode != MODE_ROLL_UP) {
+ mDisplay.erase();
+ mNonDisplay.erase();
+ }
+ // select roll-up style
+ mMode = MODE_ROLL_UP;
+ break;
+ case FON:
+ Log.i(TAG, "Flash On");
+ break;
+ case RDC:
+ // select paint-on style
+ mMode = MODE_PAINT_ON;
+ break;
+ case TR:
+ mMode = MODE_TEXT;
+ mTextMem.erase();
+ break;
+ case RTD:
+ mMode = MODE_TEXT;
+ break;
+ case EDM:
+ // erase display memory
+ mDisplay.erase();
+ updateDisplay();
+ break;
+ case CR:
+ if (mMode == MODE_ROLL_UP) {
+ getMemory().rollUp(mRollUpSize);
+ } else {
+ getMemory().cr();
+ }
+ if (mMode == MODE_ROLL_UP) {
+ updateDisplay();
+ }
+ break;
+ case ENM:
+ // erase non-display memory
+ mNonDisplay.erase();
+ break;
+ case EOC:
+ // swap display/non-display memory
+ swapMemory();
+ // switch to pop-on style
+ mMode = MODE_POP_ON;
+ updateDisplay();
+ break;
+ case INVALID:
+ default:
+ mPrevCtrlCode = INVALID;
+ return false;
+ }
+
+ mPrevCtrlCode = ctrlCode;
+
+ // handled
+ return true;
+ }
+
+ private void updateDisplay() {
+ if (mListener != null) {
+ CaptionStyle captionStyle = mListener.getCaptionStyle();
+ mListener.onDisplayChanged(mDisplay.getStyledText(captionStyle));
+ }
+ }
+
+ private void swapMemory() {
+ CCMemory temp = mDisplay;
+ mDisplay = mNonDisplay;
+ mNonDisplay = temp;
+ }
+
+ private static class StyleCode {
+ static final int COLOR_WHITE = 0;
+ static final int COLOR_GREEN = 1;
+ static final int COLOR_BLUE = 2;
+ static final int COLOR_CYAN = 3;
+ static final int COLOR_RED = 4;
+ static final int COLOR_YELLOW = 5;
+ static final int COLOR_MAGENTA = 6;
+ static final int COLOR_INVALID = 7;
+
+ static final int STYLE_ITALICS = 0x00000001;
+ static final int STYLE_UNDERLINE = 0x00000002;
+
+ static final String[] sColorMap = {
+ "WHITE", "GREEN", "BLUE", "CYAN", "RED", "YELLOW", "MAGENTA", "INVALID"
+ };
+
+ final int mStyle;
+ final int mColor;
+
+ static StyleCode fromByte(byte data2) {
+ int style = 0;
+ int color = (data2 >> 1) & 0x7;
+
+ if ((data2 & 0x1) != 0) {
+ style |= STYLE_UNDERLINE;
+ }
+
+ if (color == COLOR_INVALID) {
+ // WHITE ITALICS
+ color = COLOR_WHITE;
+ style |= STYLE_ITALICS;
+ }
+
+ return new StyleCode(style, color);
+ }
+
+ StyleCode(int style, int color) {
+ mStyle = style;
+ mColor = color;
+ }
+
+ boolean isItalics() {
+ return (mStyle & STYLE_ITALICS) != 0;
+ }
+
+ boolean isUnderline() {
+ return (mStyle & STYLE_UNDERLINE) != 0;
+ }
+
+ int getColor() {
+ return mColor;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder str = new StringBuilder();
+ str.append("{");
+ str.append(sColorMap[mColor]);
+ if ((mStyle & STYLE_ITALICS) != 0) {
+ str.append(", ITALICS");
+ }
+ if ((mStyle & STYLE_UNDERLINE) != 0) {
+ str.append(", UNDERLINE");
+ }
+ str.append("}");
+
+ return str.toString();
+ }
+ }
+
+ private static class PAC extends StyleCode {
+ final int mRow;
+ final int mCol;
+
+ static PAC fromBytes(byte data1, byte data2) {
+ int[] rowTable = {11, 1, 3, 12, 14, 5, 7, 9};
+ int row = rowTable[data1 & 0x07] + ((data2 & 0x20) >> 5);
+ int style = 0;
+ if ((data2 & 1) != 0) {
+ style |= STYLE_UNDERLINE;
+ }
+ if ((data2 & 0x10) != 0) {
+ // indent code
+ int indent = (data2 >> 1) & 0x7;
+ return new PAC(row, indent * 4, style, COLOR_WHITE);
+ } else {
+ // style code
+ int color = (data2 >> 1) & 0x7;
+
+ if (color == COLOR_INVALID) {
+ // WHITE ITALICS
+ color = COLOR_WHITE;
+ style |= STYLE_ITALICS;
+ }
+ return new PAC(row, -1, style, color);
+ }
+ }
+
+ PAC(int row, int col, int style, int color) {
+ super(style, color);
+ mRow = row;
+ mCol = col;
+ }
+
+ boolean isIndentPAC() {
+ return (mCol >= 0);
+ }
+
+ int getRow() {
+ return mRow;
+ }
+
+ int getCol() {
+ return mCol;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("{%d, %d}, %s",
+ mRow, mCol, super.toString());
+ }
+ }
+
+ /**
+ * Mutable version of BackgroundSpan to facilitate text rendering with edge styles.
+ */
+ public static class MutableBackgroundColorSpan extends CharacterStyle
+ implements UpdateAppearance {
+ private int mColor;
+
+ MutableBackgroundColorSpan(int color) {
+ mColor = color;
+ }
+
+ public void setBackgroundColor(int color) {
+ mColor = color;
+ }
+
+ public int getBackgroundColor() {
+ return mColor;
+ }
+
+ @Override
+ public void updateDrawState(TextPaint ds) {
+ ds.bgColor = mColor;
+ }
+ }
+
+ /* CCLineBuilder keeps track of displayable chars, as well as
+ * MidRow styles and PACs, for a single line of CC memory.
+ *
+ * It generates styled text via getStyledText() method.
+ */
+ private static class CCLineBuilder {
+ private final StringBuilder mDisplayChars;
+ private final StyleCode[] mMidRowStyles;
+ private final StyleCode[] mPACStyles;
+
+ CCLineBuilder(String str) {
+ mDisplayChars = new StringBuilder(str);
+ mMidRowStyles = new StyleCode[mDisplayChars.length()];
+ mPACStyles = new StyleCode[mDisplayChars.length()];
+ }
+
+ void setCharAt(int index, char ch) {
+ mDisplayChars.setCharAt(index, ch);
+ mMidRowStyles[index] = null;
+ }
+
+ void setMidRowAt(int index, StyleCode m) {
+ mDisplayChars.setCharAt(index, ' ');
+ mMidRowStyles[index] = m;
+ }
+
+ void setPACAt(int index, PAC pac) {
+ mPACStyles[index] = pac;
+ }
+
+ char charAt(int index) {
+ return mDisplayChars.charAt(index);
+ }
+
+ int length() {
+ return mDisplayChars.length();
+ }
+
+ void applyStyleSpan(
+ SpannableStringBuilder styledText,
+ StyleCode s, int start, int end) {
+ if (s.isItalics()) {
+ styledText.setSpan(
+ new StyleSpan(android.graphics.Typeface.ITALIC),
+ start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ if (s.isUnderline()) {
+ styledText.setSpan(
+ new UnderlineSpan(),
+ start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ }
+
+ SpannableStringBuilder getStyledText(CaptionStyle captionStyle) {
+ SpannableStringBuilder styledText = new SpannableStringBuilder(mDisplayChars);
+ int start = -1, next = 0;
+ int styleStart = -1;
+ StyleCode curStyle = null;
+ while (next < mDisplayChars.length()) {
+ StyleCode newStyle = null;
+ if (mMidRowStyles[next] != null) {
+ // apply mid-row style change
+ newStyle = mMidRowStyles[next];
+ } else if (mPACStyles[next] != null && (styleStart < 0 || start < 0)) {
+ // apply PAC style change, only if:
+ // 1. no style set, or
+ // 2. style set, but prev char is none-displayable
+ newStyle = mPACStyles[next];
+ }
+ if (newStyle != null) {
+ curStyle = newStyle;
+ if (styleStart >= 0 && start >= 0) {
+ applyStyleSpan(styledText, newStyle, styleStart, next);
+ }
+ styleStart = next;
+ }
+
+ if (mDisplayChars.charAt(next) != TS) {
+ if (start < 0) {
+ start = next;
+ }
+ } else if (start >= 0) {
+ int expandedStart = mDisplayChars.charAt(start) == ' ' ? start : start - 1;
+ int expandedEnd = mDisplayChars.charAt(next - 1) == ' ' ? next : next + 1;
+ styledText.setSpan(
+ new MutableBackgroundColorSpan(captionStyle.backgroundColor),
+ expandedStart, expandedEnd,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ if (styleStart >= 0) {
+ applyStyleSpan(styledText, curStyle, styleStart, expandedEnd);
+ }
+ start = -1;
+ }
+ next++;
+ }
+
+ return styledText;
+ }
+ }
+
+ /*
+ * CCMemory models a console-style display.
+ */
+ private static class CCMemory {
+ private final String mBlankLine;
+ private final CCLineBuilder[] mLines = new CCLineBuilder[MAX_ROWS + 2];
+ private int mRow;
+ private int mCol;
+
+ CCMemory() {
+ char[] blank = new char[MAX_COLS + 2];
+ Arrays.fill(blank, TS);
+ mBlankLine = new String(blank);
+ }
+
+ void erase() {
+ // erase all lines
+ for (int i = 0; i < mLines.length; i++) {
+ mLines[i] = null;
+ }
+ mRow = MAX_ROWS;
+ mCol = 1;
+ }
+
+ void der() {
+ if (mLines[mRow] != null) {
+ for (int i = 0; i < mCol; i++) {
+ if (mLines[mRow].charAt(i) != TS) {
+ for (int j = mCol; j < mLines[mRow].length(); j++) {
+ mLines[j].setCharAt(j, TS);
+ }
+ return;
+ }
+ }
+ mLines[mRow] = null;
+ }
+ }
+
+ void tab(int tabs) {
+ moveCursorByCol(tabs);
+ }
+
+ void bs() {
+ moveCursorByCol(-1);
+ if (mLines[mRow] != null) {
+ mLines[mRow].setCharAt(mCol, TS);
+ if (mCol == MAX_COLS - 1) {
+ // Spec recommendation:
+ // if cursor was at col 32, move cursor
+ // back to col 31 and erase both col 31&32
+ mLines[mRow].setCharAt(MAX_COLS, TS);
+ }
+ }
+ }
+
+ void cr() {
+ moveCursorTo(mRow + 1, 1);
+ }
+
+ void rollUp(int windowSize) {
+ int i;
+ for (i = 0; i <= mRow - windowSize; i++) {
+ mLines[i] = null;
+ }
+ int startRow = mRow - windowSize + 1;
+ if (startRow < 1) {
+ startRow = 1;
+ }
+ for (i = startRow; i < mRow; i++) {
+ mLines[i] = mLines[i + 1];
+ }
+ for (i = mRow; i < mLines.length; i++) {
+ // clear base row
+ mLines[i] = null;
+ }
+ // default to col 1, in case PAC is not sent
+ mCol = 1;
+ }
+
+ void writeText(String text) {
+ for (int i = 0; i < text.length(); i++) {
+ getLineBuffer(mRow).setCharAt(mCol, text.charAt(i));
+ moveCursorByCol(1);
+ }
+ }
+
+ void writeMidRowCode(StyleCode m) {
+ getLineBuffer(mRow).setMidRowAt(mCol, m);
+ moveCursorByCol(1);
+ }
+
+ void writePAC(PAC pac) {
+ if (pac.isIndentPAC()) {
+ moveCursorTo(pac.getRow(), pac.getCol());
+ } else {
+ moveCursorTo(pac.getRow(), 1);
+ }
+ getLineBuffer(mRow).setPACAt(mCol, pac);
+ }
+
+ SpannableStringBuilder[] getStyledText(CaptionStyle captionStyle) {
+ ArrayList<SpannableStringBuilder> rows = new ArrayList<>(MAX_ROWS);
+ for (int i = 1; i <= MAX_ROWS; i++) {
+ rows.add(mLines[i] != null ? mLines[i].getStyledText(captionStyle) : null);
+ }
+ return rows.toArray(new SpannableStringBuilder[MAX_ROWS]);
+ }
+
+ private static int clamp(int x, int min, int max) {
+ return x < min ? min : (x > max ? max : x);
+ }
+
+ private void moveCursorTo(int row, int col) {
+ mRow = clamp(row, 1, MAX_ROWS);
+ mCol = clamp(col, 1, MAX_COLS);
+ }
+
+ private void moveCursorToRow(int row) {
+ mRow = clamp(row, 1, MAX_ROWS);
+ }
+
+ private void moveCursorByCol(int col) {
+ mCol = clamp(mCol + col, 1, MAX_COLS);
+ }
+
+ private void moveBaselineTo(int baseRow, int windowSize) {
+ if (mRow == baseRow) {
+ return;
+ }
+ int actualWindowSize = windowSize;
+ if (baseRow < actualWindowSize) {
+ actualWindowSize = baseRow;
+ }
+ if (mRow < actualWindowSize) {
+ actualWindowSize = mRow;
+ }
+
+ int i;
+ if (baseRow < mRow) {
+ // copy from bottom to top row
+ for (i = actualWindowSize - 1; i >= 0; i--) {
+ mLines[baseRow - i] = mLines[mRow - i];
+ }
+ } else {
+ // copy from top to bottom row
+ for (i = 0; i < actualWindowSize; i++) {
+ mLines[baseRow - i] = mLines[mRow - i];
+ }
+ }
+ // clear rest of the rows
+ for (i = 0; i <= baseRow - windowSize; i++) {
+ mLines[i] = null;
+ }
+ for (i = baseRow + 1; i < mLines.length; i++) {
+ mLines[i] = null;
+ }
+ }
+
+ private CCLineBuilder getLineBuffer(int row) {
+ if (mLines[row] == null) {
+ mLines[row] = new CCLineBuilder(mBlankLine);
+ }
+ return mLines[row];
+ }
+ }
+
+ /*
+ * CCData parses the raw CC byte pair into displayable chars,
+ * misc control codes, Mid-Row or Preamble Address Codes.
+ */
+ private static class CCData {
+ private final byte mType;
+ private final byte mData1;
+ private final byte mData2;
+
+ private static final String[] sCtrlCodeMap = {
+ "RCL", "BS" , "AOF", "AON",
+ "DER", "RU2", "RU3", "RU4",
+ "FON", "RDC", "TR" , "RTD",
+ "EDM", "CR" , "ENM", "EOC",
+ };
+
+ private static final String[] sSpecialCharMap = {
+ "\u00AE",
+ "\u00B0",
+ "\u00BD",
+ "\u00BF",
+ "\u2122",
+ "\u00A2",
+ "\u00A3",
+ "\u266A", // Eighth note
+ "\u00E0",
+ "\u00A0", // Transparent space
+ "\u00E8",
+ "\u00E2",
+ "\u00EA",
+ "\u00EE",
+ "\u00F4",
+ "\u00FB",
+ };
+
+ private static final String[] sSpanishCharMap = {
+ // Spanish and misc chars
+ "\u00C1", // A
+ "\u00C9", // E
+ "\u00D3", // I
+ "\u00DA", // O
+ "\u00DC", // U
+ "\u00FC", // u
+ "\u2018", // opening single quote
+ "\u00A1", // inverted exclamation mark
+ "*",
+ "'",
+ "\u2014", // em dash
+ "\u00A9", // Copyright
+ "\u2120", // Servicemark
+ "\u2022", // round bullet
+ "\u201C", // opening double quote
+ "\u201D", // closing double quote
+ // French
+ "\u00C0",
+ "\u00C2",
+ "\u00C7",
+ "\u00C8",
+ "\u00CA",
+ "\u00CB",
+ "\u00EB",
+ "\u00CE",
+ "\u00CF",
+ "\u00EF",
+ "\u00D4",
+ "\u00D9",
+ "\u00F9",
+ "\u00DB",
+ "\u00AB",
+ "\u00BB"
+ };
+
+ private static final String[] sProtugueseCharMap = {
+ // Portuguese
+ "\u00C3",
+ "\u00E3",
+ "\u00CD",
+ "\u00CC",
+ "\u00EC",
+ "\u00D2",
+ "\u00F2",
+ "\u00D5",
+ "\u00F5",
+ "{",
+ "}",
+ "\\",
+ "^",
+ "_",
+ "|",
+ "~",
+ // German and misc chars
+ "\u00C4",
+ "\u00E4",
+ "\u00D6",
+ "\u00F6",
+ "\u00DF",
+ "\u00A5",
+ "\u00A4",
+ "\u2502", // vertical bar
+ "\u00C5",
+ "\u00E5",
+ "\u00D8",
+ "\u00F8",
+ "\u250C", // top-left corner
+ "\u2510", // top-right corner
+ "\u2514", // lower-left corner
+ "\u2518", // lower-right corner
+ };
+
+ static CCData[] fromByteArray(byte[] data) {
+ CCData[] ccData = new CCData[data.length / 3];
+
+ for (int i = 0; i < ccData.length; i++) {
+ ccData[i] = new CCData(
+ data[i * 3],
+ data[i * 3 + 1],
+ data[i * 3 + 2]);
+ }
+
+ return ccData;
+ }
+
+ CCData(byte type, byte data1, byte data2) {
+ mType = type;
+ mData1 = data1;
+ mData2 = data2;
+ }
+
+ int getCtrlCode() {
+ if ((mData1 == 0x14 || mData1 == 0x1c)
+ && mData2 >= 0x20 && mData2 <= 0x2f) {
+ return mData2;
+ }
+ return INVALID;
+ }
+
+ StyleCode getMidRow() {
+ // only support standard Mid-row codes, ignore
+ // optional background/foreground mid-row codes
+ if ((mData1 == 0x11 || mData1 == 0x19)
+ && mData2 >= 0x20 && mData2 <= 0x2f) {
+ return StyleCode.fromByte(mData2);
+ }
+ return null;
+ }
+
+ PAC getPAC() {
+ if ((mData1 & 0x70) == 0x10
+ && (mData2 & 0x40) == 0x40
+ && ((mData1 & 0x07) != 0 || (mData2 & 0x20) == 0)) {
+ return PAC.fromBytes(mData1, mData2);
+ }
+ return null;
+ }
+
+ int getTabOffset() {
+ if ((mData1 == 0x17 || mData1 == 0x1f)
+ && mData2 >= 0x21 && mData2 <= 0x23) {
+ return mData2 & 0x3;
+ }
+ return 0;
+ }
+
+ boolean isDisplayableChar() {
+ return isBasicChar() || isSpecialChar() || isExtendedChar();
+ }
+
+ String getDisplayText() {
+ String str = getBasicChars();
+
+ if (str == null) {
+ str = getSpecialChar();
+
+ if (str == null) {
+ str = getExtendedChar();
+ }
+ }
+
+ return str;
+ }
+
+ private String ctrlCodeToString(int ctrlCode) {
+ return sCtrlCodeMap[ctrlCode - 0x20];
+ }
+
+ private boolean isBasicChar() {
+ return mData1 >= 0x20 && mData1 <= 0x7f;
+ }
+
+ private boolean isSpecialChar() {
+ return ((mData1 == 0x11 || mData1 == 0x19)
+ && mData2 >= 0x30 && mData2 <= 0x3f);
+ }
+
+ private boolean isExtendedChar() {
+ return ((mData1 == 0x12 || mData1 == 0x1A
+ || mData1 == 0x13 || mData1 == 0x1B)
+ && mData2 >= 0x20 && mData2 <= 0x3f);
+ }
+
+ private char getBasicChar(byte data) {
+ char c;
+ // replace the non-ASCII ones
+ switch (data) {
+ case 0x2A: c = '\u00E1'; break;
+ case 0x5C: c = '\u00E9'; break;
+ case 0x5E: c = '\u00ED'; break;
+ case 0x5F: c = '\u00F3'; break;
+ case 0x60: c = '\u00FA'; break;
+ case 0x7B: c = '\u00E7'; break;
+ case 0x7C: c = '\u00F7'; break;
+ case 0x7D: c = '\u00D1'; break;
+ case 0x7E: c = '\u00F1'; break;
+ case 0x7F: c = '\u2588'; break; // Full block
+ default: c = (char) data; break;
+ }
+ return c;
+ }
+
+ private String getBasicChars() {
+ if (mData1 >= 0x20 && mData1 <= 0x7f) {
+ StringBuilder builder = new StringBuilder(2);
+ builder.append(getBasicChar(mData1));
+ if (mData2 >= 0x20 && mData2 <= 0x7f) {
+ builder.append(getBasicChar(mData2));
+ }
+ return builder.toString();
+ }
+
+ return null;
+ }
+
+ private String getSpecialChar() {
+ if ((mData1 == 0x11 || mData1 == 0x19)
+ && mData2 >= 0x30 && mData2 <= 0x3f) {
+ return sSpecialCharMap[mData2 - 0x30];
+ }
+
+ return null;
+ }
+
+ private String getExtendedChar() {
+ if ((mData1 == 0x12 || mData1 == 0x1A) && mData2 >= 0x20 && mData2 <= 0x3f) {
+ // 1 Spanish/French char
+ return sSpanishCharMap[mData2 - 0x20];
+ } else if ((mData1 == 0x13 || mData1 == 0x1B) && mData2 >= 0x20 && mData2 <= 0x3f) {
+ // 1 Portuguese/German/Danish char
+ return sProtugueseCharMap[mData2 - 0x20];
+ }
+
+ return null;
+ }
+
+ @Override
+ public String toString() {
+ String str;
+
+ if (mData1 < 0x10 && mData2 < 0x10) {
+ // Null Pad, ignore
+ return String.format("[%d]Null: %02x %02x", mType, mData1, mData2);
+ }
+
+ int ctrlCode = getCtrlCode();
+ if (ctrlCode != INVALID) {
+ return String.format("[%d]%s", mType, ctrlCodeToString(ctrlCode));
+ }
+
+ int tabOffset = getTabOffset();
+ if (tabOffset > 0) {
+ return String.format("[%d]Tab%d", mType, tabOffset);
+ }
+
+ PAC pac = getPAC();
+ if (pac != null) {
+ return String.format("[%d]PAC: %s", mType, pac.toString());
+ }
+
+ StyleCode m = getMidRow();
+ if (m != null) {
+ return String.format("[%d]Mid-row: %s", mType, m.toString());
+ }
+
+ if (isDisplayableChar()) {
+ return String.format("[%d]Displayable: %s (%02x %02x)",
+ mType, getDisplayText(), mData1, mData2);
+ }
+
+ return String.format("[%d]Invalid: %02x %02x", mType, mData1, mData2);
+ }
+ }
+}
diff --git a/media/src/main/java/androidx/media/subtitle/ClosedCaptionRenderer.java b/media/src/main/java/androidx/media/subtitle/ClosedCaptionRenderer.java
new file mode 100644
index 0000000..90ff516
--- /dev/null
+++ b/media/src/main/java/androidx/media/subtitle/ClosedCaptionRenderer.java
@@ -0,0 +1,402 @@
+/*
+ * 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 androidx.media.subtitle;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.Typeface;
+import android.media.MediaFormat;
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
+import android.text.TextPaint;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.View;
+import android.view.accessibility.CaptioningManager.CaptionStyle;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.media.R;
+
+import java.util.ArrayList;
+
+// Note: This is forked from android.media.ClosedCaptionRenderer since P
+/**
+ * @hide
+ */
+@RequiresApi(28)
+@RestrictTo(LIBRARY_GROUP)
+public class ClosedCaptionRenderer extends SubtitleController.Renderer {
+ private final Context mContext;
+ private Cea608CCWidget mCCWidget;
+
+ public ClosedCaptionRenderer(Context context) {
+ mContext = context;
+ }
+
+ @Override
+ public boolean supports(MediaFormat format) {
+ if (format.containsKey(MediaFormat.KEY_MIME)) {
+ String mimeType = format.getString(MediaFormat.KEY_MIME);
+ return MediaFormat.MIMETYPE_TEXT_CEA_608.equals(mimeType);
+ }
+ return false;
+ }
+
+ @Override
+ public SubtitleTrack createTrack(MediaFormat format) {
+ String mimeType = format.getString(MediaFormat.KEY_MIME);
+ if (MediaFormat.MIMETYPE_TEXT_CEA_608.equals(mimeType)) {
+ if (mCCWidget == null) {
+ mCCWidget = new Cea608CCWidget(mContext);
+ }
+ return new Cea608CaptionTrack(mCCWidget, format);
+ }
+ throw new RuntimeException("No matching format: " + format.toString());
+ }
+
+ static class Cea608CaptionTrack extends SubtitleTrack {
+ private final Cea608CCParser mCCParser;
+ private final Cea608CCWidget mRenderingWidget;
+
+ Cea608CaptionTrack(Cea608CCWidget renderingWidget, MediaFormat format) {
+ super(format);
+
+ mRenderingWidget = renderingWidget;
+ mCCParser = new Cea608CCParser(mRenderingWidget);
+ }
+
+ @Override
+ public void onData(byte[] data, boolean eos, long runID) {
+ mCCParser.parse(data);
+ }
+
+ @Override
+ public RenderingWidget getRenderingWidget() {
+ return mRenderingWidget;
+ }
+
+ @Override
+ public void updateView(ArrayList<Cue> activeCues) {
+ // Overriding with NO-OP, CC rendering by-passes this
+ }
+ }
+
+ /**
+ * Widget capable of rendering CEA-608 closed captions.
+ */
+ class Cea608CCWidget extends ClosedCaptionWidget implements Cea608CCParser.DisplayListener {
+ private static final String DUMMY_TEXT = "1234567890123456789012345678901234";
+ private final Rect mTextBounds = new Rect();
+
+ Cea608CCWidget(Context context) {
+ this(context, null);
+ }
+
+ Cea608CCWidget(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ Cea608CCWidget(Context context, AttributeSet attrs, int defStyle) {
+ this(context, attrs, defStyle, 0);
+ }
+
+ Cea608CCWidget(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ public ClosedCaptionLayout createCaptionLayout(Context context) {
+ return new CCLayout(context);
+ }
+
+ @Override
+ public void onDisplayChanged(SpannableStringBuilder[] styledTexts) {
+ ((CCLayout) mClosedCaptionLayout).update(styledTexts);
+
+ if (mListener != null) {
+ mListener.onChanged(this);
+ }
+ }
+
+ @Override
+ public CaptionStyle getCaptionStyle() {
+ return mCaptionStyle;
+ }
+
+ private class CCLineBox extends TextView {
+ private static final float FONT_PADDING_RATIO = 0.75f;
+ private static final float EDGE_OUTLINE_RATIO = 0.1f;
+ private static final float EDGE_SHADOW_RATIO = 0.05f;
+ private float mOutlineWidth;
+ private float mShadowRadius;
+ private float mShadowOffset;
+
+ private int mTextColor = Color.WHITE;
+ private int mBgColor = Color.BLACK;
+ private int mEdgeType = CaptionStyle.EDGE_TYPE_NONE;
+ private int mEdgeColor = Color.TRANSPARENT;
+
+ CCLineBox(Context context) {
+ super(context);
+ setGravity(Gravity.CENTER);
+ setBackgroundColor(Color.TRANSPARENT);
+ setTextColor(Color.WHITE);
+ setTypeface(Typeface.MONOSPACE);
+ setVisibility(View.INVISIBLE);
+
+ final Resources res = getContext().getResources();
+
+ // get the default (will be updated later during measure)
+ mOutlineWidth = res.getDimensionPixelSize(
+ R.dimen.subtitle_outline_width);
+ mShadowRadius = res.getDimensionPixelSize(
+ R.dimen.subtitle_shadow_radius);
+ mShadowOffset = res.getDimensionPixelSize(
+ R.dimen.subtitle_shadow_offset);
+ }
+
+ void setCaptionStyle(CaptionStyle captionStyle) {
+ mTextColor = captionStyle.foregroundColor;
+ mBgColor = captionStyle.backgroundColor;
+ mEdgeType = captionStyle.edgeType;
+ mEdgeColor = captionStyle.edgeColor;
+
+ setTextColor(mTextColor);
+ if (mEdgeType == CaptionStyle.EDGE_TYPE_DROP_SHADOW) {
+ setShadowLayer(mShadowRadius, mShadowOffset, mShadowOffset, mEdgeColor);
+ } else {
+ setShadowLayer(0, 0, 0, 0);
+ }
+ invalidate();
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ float fontSize = MeasureSpec.getSize(heightMeasureSpec) * FONT_PADDING_RATIO;
+ setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize);
+
+ mOutlineWidth = EDGE_OUTLINE_RATIO * fontSize + 1.0f;
+ mShadowRadius = EDGE_SHADOW_RATIO * fontSize + 1.0f;
+ mShadowOffset = mShadowRadius;
+
+ // set font scale in the X direction to match the required width
+ setScaleX(1.0f);
+ getPaint().getTextBounds(DUMMY_TEXT, 0, DUMMY_TEXT.length(), mTextBounds);
+ float actualTextWidth = mTextBounds.width();
+ float requiredTextWidth = MeasureSpec.getSize(widthMeasureSpec);
+ setScaleX(requiredTextWidth / actualTextWidth);
+
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+
+ @Override
+ protected void onDraw(Canvas c) {
+ if (mEdgeType == CaptionStyle.EDGE_TYPE_UNSPECIFIED
+ || mEdgeType == CaptionStyle.EDGE_TYPE_NONE
+ || mEdgeType == CaptionStyle.EDGE_TYPE_DROP_SHADOW) {
+ // these edge styles don't require a second pass
+ super.onDraw(c);
+ return;
+ }
+
+ if (mEdgeType == CaptionStyle.EDGE_TYPE_OUTLINE) {
+ drawEdgeOutline(c);
+ } else {
+ // Raised or depressed
+ drawEdgeRaisedOrDepressed(c);
+ }
+ }
+
+ @SuppressWarnings("WrongCall")
+ private void drawEdgeOutline(Canvas c) {
+ TextPaint textPaint = getPaint();
+
+ Paint.Style previousStyle = textPaint.getStyle();
+ Paint.Join previousJoin = textPaint.getStrokeJoin();
+ float previousWidth = textPaint.getStrokeWidth();
+
+ setTextColor(mEdgeColor);
+ textPaint.setStyle(Paint.Style.FILL_AND_STROKE);
+ textPaint.setStrokeJoin(Paint.Join.ROUND);
+ textPaint.setStrokeWidth(mOutlineWidth);
+
+ // Draw outline and background only.
+ super.onDraw(c);
+
+ // Restore original settings.
+ setTextColor(mTextColor);
+ textPaint.setStyle(previousStyle);
+ textPaint.setStrokeJoin(previousJoin);
+ textPaint.setStrokeWidth(previousWidth);
+
+ // Remove the background.
+ setBackgroundSpans(Color.TRANSPARENT);
+ // Draw foreground only.
+ super.onDraw(c);
+ // Restore the background.
+ setBackgroundSpans(mBgColor);
+ }
+
+ @SuppressWarnings("WrongCall")
+ private void drawEdgeRaisedOrDepressed(Canvas c) {
+ TextPaint textPaint = getPaint();
+
+ Paint.Style previousStyle = textPaint.getStyle();
+ textPaint.setStyle(Paint.Style.FILL);
+
+ final boolean raised = mEdgeType == CaptionStyle.EDGE_TYPE_RAISED;
+ final int colorUp = raised ? Color.WHITE : mEdgeColor;
+ final int colorDown = raised ? mEdgeColor : Color.WHITE;
+ final float offset = mShadowRadius / 2f;
+
+ // Draw background and text with shadow up
+ setShadowLayer(mShadowRadius, -offset, -offset, colorUp);
+ super.onDraw(c);
+
+ // Remove the background.
+ setBackgroundSpans(Color.TRANSPARENT);
+
+ // Draw text with shadow down
+ setShadowLayer(mShadowRadius, +offset, +offset, colorDown);
+ super.onDraw(c);
+
+ // Restore settings
+ textPaint.setStyle(previousStyle);
+
+ // Restore the background.
+ setBackgroundSpans(mBgColor);
+ }
+
+ private void setBackgroundSpans(int color) {
+ CharSequence text = getText();
+ if (text instanceof Spannable) {
+ Spannable spannable = (Spannable) text;
+ Cea608CCParser.MutableBackgroundColorSpan[] bgSpans = spannable.getSpans(
+ 0, spannable.length(), Cea608CCParser.MutableBackgroundColorSpan.class);
+ for (int i = 0; i < bgSpans.length; i++) {
+ bgSpans[i].setBackgroundColor(color);
+ }
+ }
+ }
+ }
+
+ private class CCLayout extends LinearLayout implements ClosedCaptionLayout {
+ private static final int MAX_ROWS = Cea608CCParser.MAX_ROWS;
+ private static final float SAFE_AREA_RATIO = 0.9f;
+
+ private final CCLineBox[] mLineBoxes = new CCLineBox[MAX_ROWS];
+
+ CCLayout(Context context) {
+ super(context);
+ setGravity(Gravity.START);
+ setOrientation(LinearLayout.VERTICAL);
+ for (int i = 0; i < MAX_ROWS; i++) {
+ mLineBoxes[i] = new CCLineBox(getContext());
+ addView(mLineBoxes[i], LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+ }
+ }
+
+ @Override
+ public void setCaptionStyle(CaptionStyle captionStyle) {
+ for (int i = 0; i < MAX_ROWS; i++) {
+ mLineBoxes[i].setCaptionStyle(captionStyle);
+ }
+ }
+
+ @Override
+ public void setFontScale(float fontScale) {
+ // Ignores the font scale changes of the system wide CC preference.
+ }
+
+ void update(SpannableStringBuilder[] textBuffer) {
+ for (int i = 0; i < MAX_ROWS; i++) {
+ if (textBuffer[i] != null) {
+ mLineBoxes[i].setText(textBuffer[i], TextView.BufferType.SPANNABLE);
+ mLineBoxes[i].setVisibility(View.VISIBLE);
+ } else {
+ mLineBoxes[i].setVisibility(View.INVISIBLE);
+ }
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+ int safeWidth = getMeasuredWidth();
+ int safeHeight = getMeasuredHeight();
+
+ // CEA-608 assumes 4:3 video
+ if (safeWidth * 3 >= safeHeight * 4) {
+ safeWidth = safeHeight * 4 / 3;
+ } else {
+ safeHeight = safeWidth * 3 / 4;
+ }
+ safeWidth = (int) (safeWidth * SAFE_AREA_RATIO);
+ safeHeight = (int) (safeHeight * SAFE_AREA_RATIO);
+
+ int lineHeight = safeHeight / MAX_ROWS;
+ int lineHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
+ lineHeight, MeasureSpec.EXACTLY);
+ int lineWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
+ safeWidth, MeasureSpec.EXACTLY);
+
+ for (int i = 0; i < MAX_ROWS; i++) {
+ mLineBoxes[i].measure(lineWidthMeasureSpec, lineHeightMeasureSpec);
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ // safe caption area
+ int viewPortWidth = r - l;
+ int viewPortHeight = b - t;
+ int safeWidth, safeHeight;
+ // CEA-608 assumes 4:3 video
+ if (viewPortWidth * 3 >= viewPortHeight * 4) {
+ safeWidth = viewPortHeight * 4 / 3;
+ safeHeight = viewPortHeight;
+ } else {
+ safeWidth = viewPortWidth;
+ safeHeight = viewPortWidth * 3 / 4;
+ }
+ safeWidth = (int) (safeWidth * SAFE_AREA_RATIO);
+ safeHeight = (int) (safeHeight * SAFE_AREA_RATIO);
+ int left = (viewPortWidth - safeWidth) / 2;
+ int top = (viewPortHeight - safeHeight) / 2;
+
+ for (int i = 0; i < MAX_ROWS; i++) {
+ mLineBoxes[i].layout(
+ left,
+ top + safeHeight * i / MAX_ROWS,
+ left + safeWidth,
+ top + safeHeight * (i + 1) / MAX_ROWS);
+ }
+ }
+ }
+ }
+}
diff --git a/media/src/main/java/androidx/media/subtitle/ClosedCaptionWidget.java b/media/src/main/java/androidx/media/subtitle/ClosedCaptionWidget.java
new file mode 100644
index 0000000..a3d3e47
--- /dev/null
+++ b/media/src/main/java/androidx/media/subtitle/ClosedCaptionWidget.java
@@ -0,0 +1,167 @@
+/*
+ * 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 androidx.media.subtitle;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.accessibility.CaptioningManager;
+import android.view.accessibility.CaptioningManager.CaptionStyle;
+import android.view.accessibility.CaptioningManager.CaptioningChangeListener;
+
+import androidx.annotation.RequiresApi;
+
+/**
+ * Abstract widget class to render a closed caption track.
+ */
+@RequiresApi(28)
+abstract class ClosedCaptionWidget extends ViewGroup implements SubtitleTrack.RenderingWidget {
+
+ interface ClosedCaptionLayout {
+ void setCaptionStyle(CaptionStyle captionStyle);
+ void setFontScale(float scale);
+ }
+
+ /** Captioning manager, used to obtain and track caption properties. */
+ private final CaptioningManager mManager;
+
+ /** Current caption style. */
+ protected CaptionStyle mCaptionStyle;
+
+ /** Callback for rendering changes. */
+ protected OnChangedListener mListener;
+
+ /** Concrete layout of CC. */
+ protected ClosedCaptionLayout mClosedCaptionLayout;
+
+ /** Whether a caption style change listener is registered. */
+ private boolean mHasChangeListener;
+
+ ClosedCaptionWidget(Context context) {
+ this(context, null);
+ }
+
+ ClosedCaptionWidget(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ ClosedCaptionWidget(Context context, AttributeSet attrs, int defStyle) {
+ this(context, attrs, defStyle, 0);
+ }
+
+ ClosedCaptionWidget(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ // Cannot render text over video when layer type is hardware.
+ setLayerType(View.LAYER_TYPE_SOFTWARE, null);
+
+ mManager = (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE);
+ mCaptionStyle = mManager.getUserStyle();
+
+ mClosedCaptionLayout = createCaptionLayout(context);
+ mClosedCaptionLayout.setCaptionStyle(mCaptionStyle);
+ mClosedCaptionLayout.setFontScale(mManager.getFontScale());
+ addView((ViewGroup) mClosedCaptionLayout, LayoutParams.MATCH_PARENT,
+ LayoutParams.MATCH_PARENT);
+
+ requestLayout();
+ }
+
+ public abstract ClosedCaptionLayout createCaptionLayout(Context context);
+
+ @Override
+ public void setOnChangedListener(OnChangedListener listener) {
+ mListener = listener;
+ }
+
+ @Override
+ public void setSize(int width, int height) {
+ final int widthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
+ final int heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
+
+ measure(widthSpec, heightSpec);
+ layout(0, 0, width, height);
+ }
+
+ @Override
+ public void setVisible(boolean visible) {
+ if (visible) {
+ setVisibility(View.VISIBLE);
+ } else {
+ setVisibility(View.GONE);
+ }
+
+ manageChangeListener();
+ }
+
+ @Override
+ public void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ manageChangeListener();
+ }
+
+ @Override
+ public void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+
+ manageChangeListener();
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ ((ViewGroup) mClosedCaptionLayout).measure(widthMeasureSpec, heightMeasureSpec);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ ((ViewGroup) mClosedCaptionLayout).layout(l, t, r, b);
+ }
+
+ /**
+ * Manages whether this renderer is listening for caption style changes.
+ */
+ private final CaptioningChangeListener mCaptioningListener = new CaptioningChangeListener() {
+ @Override
+ public void onUserStyleChanged(CaptionStyle userStyle) {
+ mCaptionStyle = userStyle;
+ mClosedCaptionLayout.setCaptionStyle(mCaptionStyle);
+ }
+
+ @Override
+ public void onFontScaleChanged(float fontScale) {
+ mClosedCaptionLayout.setFontScale(fontScale);
+ }
+ };
+
+ private void manageChangeListener() {
+ final boolean needsListener = isAttachedToWindow() && getVisibility() == View.VISIBLE;
+ if (mHasChangeListener != needsListener) {
+ mHasChangeListener = needsListener;
+
+ if (needsListener) {
+ mManager.addCaptioningChangeListener(mCaptioningListener);
+ } else {
+ mManager.removeCaptioningChangeListener(mCaptioningListener);
+ }
+ }
+ }
+}
+
diff --git a/media/src/main/java/androidx/media/subtitle/MediaTimeProvider.java b/media/src/main/java/androidx/media/subtitle/MediaTimeProvider.java
new file mode 100644
index 0000000..b6f0a14
--- /dev/null
+++ b/media/src/main/java/androidx/media/subtitle/MediaTimeProvider.java
@@ -0,0 +1,100 @@
+/*
+ * 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 androidx.media.subtitle;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import androidx.annotation.RestrictTo;
+
+// Note: This is just copied from android.media.MediaTimeProvider.
+/**
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+public interface MediaTimeProvider {
+ // we do not allow negative media time
+ /**
+ * Presentation time value if no timed event notification is requested.
+ */
+ long NO_TIME = -1;
+
+ /**
+ * Cancels all previous notification request from this listener if any. It
+ * registers the listener to get seek and stop notifications. If timeUs is
+ * not negative, it also registers the listener for a timed event
+ * notification when the presentation time reaches (becomes greater) than
+ * the value specified. This happens immediately if the current media time
+ * is larger than or equal to timeUs.
+ *
+ * @param timeUs presentation time to get timed event callback at (or
+ * {@link #NO_TIME})
+ */
+ void notifyAt(long timeUs, OnMediaTimeListener listener);
+
+ /**
+ * Cancels all previous notification request from this listener if any. It
+ * registers the listener to get seek and stop notifications. If the media
+ * is stopped, the listener will immediately receive a stop notification.
+ * Otherwise, it will receive a timed event notificaton.
+ */
+ void scheduleUpdate(OnMediaTimeListener listener);
+
+ /**
+ * Cancels all previous notification request from this listener if any.
+ */
+ void cancelNotifications(OnMediaTimeListener listener);
+
+ /**
+ * Get the current presentation time.
+ *
+ * @param precise Whether getting a precise time is important. This is
+ * more costly.
+ * @param monotonic Whether returned time should be monotonic: that is,
+ * greater than or equal to the last returned time. Don't
+ * always set this to true. E.g. this has undesired
+ * consequences if the media is seeked between calls.
+ * @throws IllegalStateException if the media is not initialized
+ */
+ long getCurrentTimeUs(boolean precise, boolean monotonic)
+ throws IllegalStateException;
+
+ /**
+ * Mediatime listener
+ */
+ public interface OnMediaTimeListener {
+ /**
+ * Called when the registered time was reached naturally.
+ *
+ * @param timeUs current media time
+ */
+ void onTimedEvent(long timeUs);
+
+ /**
+ * Called when the media time changed due to seeking.
+ *
+ * @param timeUs current media time
+ */
+ void onSeek(long timeUs);
+
+ /**
+ * Called when the playback stopped. This is not called on pause, only
+ * on full stop, at which point there is no further current media time.
+ */
+ void onStop();
+ }
+}
+
diff --git a/media/src/main/java/androidx/media/subtitle/SubtitleController.java b/media/src/main/java/androidx/media/subtitle/SubtitleController.java
new file mode 100644
index 0000000..b6dfc2b
--- /dev/null
+++ b/media/src/main/java/androidx/media/subtitle/SubtitleController.java
@@ -0,0 +1,534 @@
+/*
+ * 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 androidx.media.subtitle;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.content.Context;
+import android.media.MediaFormat;
+import android.media.MediaPlayer;
+import android.media.MediaPlayer.TrackInfo;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.view.accessibility.CaptioningManager;
+
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.media.subtitle.SubtitleTrack.RenderingWidget;
+
+import java.util.ArrayList;
+import java.util.Locale;
+
+// Note: This is forked from android.media.SubtitleController since P
+/**
+ * The subtitle controller provides the architecture to display subtitles for a
+ * media source. It allows specifying which tracks to display, on which anchor
+ * to display them, and also allows adding external, out-of-band subtitle tracks.
+ *
+ * @hide
+ */
+@RequiresApi(28)
+@RestrictTo(LIBRARY_GROUP)
+public class SubtitleController {
+ private MediaTimeProvider mTimeProvider;
+ private ArrayList<Renderer> mRenderers;
+ private ArrayList<SubtitleTrack> mTracks;
+ private final Object mRenderersLock = new Object();
+ private final Object mTracksLock = new Object();
+ private SubtitleTrack mSelectedTrack;
+ private boolean mShowing;
+ private CaptioningManager mCaptioningManager;
+ private Handler mHandler;
+
+ private static final int WHAT_SHOW = 1;
+ private static final int WHAT_HIDE = 2;
+ private static final int WHAT_SELECT_TRACK = 3;
+ private static final int WHAT_SELECT_DEFAULT_TRACK = 4;
+
+ private final Handler.Callback mCallback = new Handler.Callback() {
+ @Override
+ public boolean handleMessage(Message msg) {
+ switch (msg.what) {
+ case WHAT_SHOW:
+ doShow();
+ return true;
+ case WHAT_HIDE:
+ doHide();
+ return true;
+ case WHAT_SELECT_TRACK:
+ doSelectTrack((SubtitleTrack) msg.obj);
+ return true;
+ case WHAT_SELECT_DEFAULT_TRACK:
+ doSelectDefaultTrack();
+ return true;
+ default:
+ return false;
+ }
+ }
+ };
+
+ private CaptioningManager.CaptioningChangeListener mCaptioningChangeListener =
+ new CaptioningManager.CaptioningChangeListener() {
+ @Override
+ public void onEnabledChanged(boolean enabled) {
+ selectDefaultTrack();
+ }
+
+ @Override
+ public void onLocaleChanged(Locale locale) {
+ selectDefaultTrack();
+ }
+ };
+
+ public SubtitleController(Context context) {
+ this(context, null, null);
+ }
+
+ /**
+ * Creates a subtitle controller for a media playback object that implements
+ * the MediaTimeProvider interface.
+ *
+ * @param timeProvider
+ */
+ public SubtitleController(
+ Context context,
+ MediaTimeProvider timeProvider,
+ Listener listener) {
+ mTimeProvider = timeProvider;
+ mListener = listener;
+
+ mRenderers = new ArrayList<Renderer>();
+ mShowing = false;
+ mTracks = new ArrayList<SubtitleTrack>();
+ mCaptioningManager =
+ (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE);
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ mCaptioningManager.removeCaptioningChangeListener(
+ mCaptioningChangeListener);
+ super.finalize();
+ }
+
+ /**
+ * @return the available subtitle tracks for this media. These include
+ * the tracks found by {@link MediaPlayer} as well as any tracks added
+ * manually via {@link #addTrack}.
+ */
+ public SubtitleTrack[] getTracks() {
+ synchronized (mTracksLock) {
+ SubtitleTrack[] tracks = new SubtitleTrack[mTracks.size()];
+ mTracks.toArray(tracks);
+ return tracks;
+ }
+ }
+
+ /**
+ * @return the currently selected subtitle track
+ */
+ public SubtitleTrack getSelectedTrack() {
+ return mSelectedTrack;
+ }
+
+ private RenderingWidget getRenderingWidget() {
+ if (mSelectedTrack == null) {
+ return null;
+ }
+ return mSelectedTrack.getRenderingWidget();
+ }
+
+ /**
+ * Selects a subtitle track. As a result, this track will receive
+ * in-band data from the {@link MediaPlayer}. However, this does
+ * not change the subtitle visibility.
+ *
+ * Should be called from the anchor's (UI) thread. {@see #Anchor.getSubtitleLooper}
+ *
+ * @param track The subtitle track to select. This must be one of the
+ * tracks in {@link #getTracks}.
+ * @return true if the track was successfully selected.
+ */
+ public boolean selectTrack(SubtitleTrack track) {
+ if (track != null && !mTracks.contains(track)) {
+ return false;
+ }
+
+ processOnAnchor(mHandler.obtainMessage(WHAT_SELECT_TRACK, track));
+ return true;
+ }
+
+ private void doSelectTrack(SubtitleTrack track) {
+ mTrackIsExplicit = true;
+ if (mSelectedTrack == track) {
+ return;
+ }
+
+ if (mSelectedTrack != null) {
+ mSelectedTrack.hide();
+ mSelectedTrack.setTimeProvider(null);
+ }
+
+ mSelectedTrack = track;
+ if (mAnchor != null) {
+ mAnchor.setSubtitleWidget(getRenderingWidget());
+ }
+
+ if (mSelectedTrack != null) {
+ mSelectedTrack.setTimeProvider(mTimeProvider);
+ mSelectedTrack.show();
+ }
+
+ if (mListener != null) {
+ mListener.onSubtitleTrackSelected(track);
+ }
+ }
+
+ /**
+ * @return the default subtitle track based on system preferences, or null,
+ * if no such track exists in this manager.
+ *
+ * Supports HLS-flags: AUTOSELECT, FORCED & DEFAULT.
+ *
+ * 1. If captioning is disabled, only consider FORCED tracks. Otherwise,
+ * consider all tracks, but prefer non-FORCED ones.
+ * 2. If user selected "Default" caption language:
+ * a. If there is a considered track with DEFAULT=yes, returns that track
+ * (favor the first one in the current language if there are more than
+ * one default tracks, or the first in general if none of them are in
+ * the current language).
+ * b. Otherwise, if there is a track with AUTOSELECT=yes in the current
+ * language, return that one.
+ * c. If there are no default tracks, and no autoselectable tracks in the
+ * current language, return null.
+ * 3. If there is a track with the caption language, select that one. Prefer
+ * the one with AUTOSELECT=no.
+ *
+ * The default values for these flags are DEFAULT=no, AUTOSELECT=yes
+ * and FORCED=no.
+ */
+ public SubtitleTrack getDefaultTrack() {
+ SubtitleTrack bestTrack = null;
+ int bestScore = -1;
+
+ Locale selectedLocale = mCaptioningManager.getLocale();
+ Locale locale = selectedLocale;
+ if (locale == null) {
+ locale = Locale.getDefault();
+ }
+ boolean selectForced = !mCaptioningManager.isEnabled();
+
+ synchronized (mTracksLock) {
+ for (SubtitleTrack track: mTracks) {
+ MediaFormat format = track.getFormat();
+ String language = format.getString(MediaFormat.KEY_LANGUAGE);
+ boolean forced = MediaFormatUtil
+ .getInteger(format, MediaFormat.KEY_IS_FORCED_SUBTITLE, 0) != 0;
+ boolean autoselect = MediaFormatUtil
+ .getInteger(format, MediaFormat.KEY_IS_AUTOSELECT, 1) != 0;
+ boolean is_default = MediaFormatUtil
+ .getInteger(format, MediaFormat.KEY_IS_DEFAULT, 0) != 0;
+
+ boolean languageMatches = locale == null
+ || locale.getLanguage().equals("")
+ || locale.getISO3Language().equals(language)
+ || locale.getLanguage().equals(language);
+ // is_default is meaningless unless caption language is 'default'
+ int score = (forced ? 0 : 8)
+ + (((selectedLocale == null) && is_default) ? 4 : 0)
+ + (autoselect ? 0 : 2) + (languageMatches ? 1 : 0);
+
+ if (selectForced && !forced) {
+ continue;
+ }
+
+ // we treat null locale/language as matching any language
+ if ((selectedLocale == null && is_default)
+ || (languageMatches && (autoselect || forced || selectedLocale != null))) {
+ if (score > bestScore) {
+ bestScore = score;
+ bestTrack = track;
+ }
+ }
+ }
+ }
+ return bestTrack;
+ }
+
+ static class MediaFormatUtil {
+ MediaFormatUtil() { }
+ static int getInteger(MediaFormat format, String name, int defaultValue) {
+ try {
+ return format.getInteger(name);
+ } catch (NullPointerException | ClassCastException e) {
+ /* no such field or field of different type */
+ }
+ return defaultValue;
+ }
+ }
+
+ private boolean mTrackIsExplicit = false;
+ private boolean mVisibilityIsExplicit = false;
+
+ /** should be called from anchor thread */
+ public void selectDefaultTrack() {
+ processOnAnchor(mHandler.obtainMessage(WHAT_SELECT_DEFAULT_TRACK));
+ }
+
+ private void doSelectDefaultTrack() {
+ if (mTrackIsExplicit) {
+ if (mVisibilityIsExplicit) {
+ return;
+ }
+ // If track selection is explicit, but visibility
+ // is not, it falls back to the captioning setting
+ if (mCaptioningManager.isEnabled()
+ || (mSelectedTrack != null && MediaFormatUtil.getInteger(
+ mSelectedTrack.getFormat(),
+ MediaFormat.KEY_IS_FORCED_SUBTITLE, 0) != 0)) {
+ show();
+ } else if (mSelectedTrack != null
+ && mSelectedTrack.getTrackType() == TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE) {
+ hide();
+ }
+ mVisibilityIsExplicit = false;
+ }
+
+ // We can have a default (forced) track even if captioning
+ // is not enabled. This is handled by getDefaultTrack().
+ // Show this track unless subtitles were explicitly hidden.
+ SubtitleTrack track = getDefaultTrack();
+ if (track != null) {
+ selectTrack(track);
+ mTrackIsExplicit = false;
+ if (!mVisibilityIsExplicit) {
+ show();
+ mVisibilityIsExplicit = false;
+ }
+ }
+ }
+
+ /** must be called from anchor thread */
+ public void reset() {
+ checkAnchorLooper();
+ hide();
+ selectTrack(null);
+ mTracks.clear();
+ mTrackIsExplicit = false;
+ mVisibilityIsExplicit = false;
+ mCaptioningManager.removeCaptioningChangeListener(
+ mCaptioningChangeListener);
+ }
+
+ /**
+ * Adds a new, external subtitle track to the manager.
+ *
+ * @param format the format of the track that will include at least
+ * the MIME type {@link MediaFormat@KEY_MIME}.
+ * @return the created {@link SubtitleTrack} object
+ */
+ public SubtitleTrack addTrack(MediaFormat format) {
+ synchronized (mRenderersLock) {
+ for (Renderer renderer: mRenderers) {
+ if (renderer.supports(format)) {
+ SubtitleTrack track = renderer.createTrack(format);
+ if (track != null) {
+ synchronized (mTracksLock) {
+ if (mTracks.size() == 0) {
+ mCaptioningManager.addCaptioningChangeListener(
+ mCaptioningChangeListener);
+ }
+ mTracks.add(track);
+ }
+ return track;
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Show the selected (or default) subtitle track.
+ *
+ * Should be called from the anchor's (UI) thread. {@see #Anchor.getSubtitleLooper}
+ */
+ public void show() {
+ processOnAnchor(mHandler.obtainMessage(WHAT_SHOW));
+ }
+
+ private void doShow() {
+ mShowing = true;
+ mVisibilityIsExplicit = true;
+ if (mSelectedTrack != null) {
+ mSelectedTrack.show();
+ }
+ }
+
+ /**
+ * Hide the selected (or default) subtitle track.
+ *
+ * Should be called from the anchor's (UI) thread. {@see #Anchor.getSubtitleLooper}
+ */
+ public void hide() {
+ processOnAnchor(mHandler.obtainMessage(WHAT_HIDE));
+ }
+
+ private void doHide() {
+ mVisibilityIsExplicit = true;
+ if (mSelectedTrack != null) {
+ mSelectedTrack.hide();
+ }
+ mShowing = false;
+ }
+
+ /**
+ * Interface for supporting a single or multiple subtitle types in {@link MediaPlayer}.
+ */
+ public abstract static class Renderer {
+ /**
+ * Called by {@link MediaPlayer}'s {@link SubtitleController} when a new
+ * subtitle track is detected, to see if it should use this object to
+ * parse and display this subtitle track.
+ *
+ * @param format the format of the track that will include at least
+ * the MIME type {@link MediaFormat@KEY_MIME}.
+ *
+ * @return true if and only if the track format is supported by this
+ * renderer
+ */
+ public abstract boolean supports(MediaFormat format);
+
+ /**
+ * Called by {@link MediaPlayer}'s {@link SubtitleController} for each
+ * subtitle track that was detected and is supported by this object to
+ * create a {@link SubtitleTrack} object. This object will be created
+ * for each track that was found. If the track is selected for display,
+ * this object will be used to parse and display the track data.
+ *
+ * @param format the format of the track that will include at least
+ * the MIME type {@link MediaFormat@KEY_MIME}.
+ * @return a {@link SubtitleTrack} object that will be used to parse
+ * and render the subtitle track.
+ */
+ public abstract SubtitleTrack createTrack(MediaFormat format);
+ }
+
+ /**
+ * Add support for a subtitle format in {@link MediaPlayer}.
+ *
+ * @param renderer a {@link SubtitleController.Renderer} object that adds
+ * support for a subtitle format.
+ */
+ public void registerRenderer(Renderer renderer) {
+ synchronized (mRenderersLock) {
+ // TODO how to get available renderers in the system
+ if (!mRenderers.contains(renderer)) {
+ // TODO should added renderers override existing ones (to allow replacing?)
+ mRenderers.add(renderer);
+ }
+ }
+ }
+
+ /**
+ * Returns true if one of the registered renders supports given media format.
+ *
+ * @param format a {@link MediaFormat} object
+ * @return true if this SubtitleController has a renderer that supports
+ * the media format.
+ */
+ public boolean hasRendererFor(MediaFormat format) {
+ synchronized (mRenderersLock) {
+ // TODO how to get available renderers in the system
+ for (Renderer renderer: mRenderers) {
+ if (renderer.supports(format)) {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+
+ /**
+ * Subtitle anchor, an object that is able to display a subtitle renderer,
+ * e.g. a VideoView.
+ */
+ public interface Anchor {
+ /**
+ * Anchor should use the supplied subtitle rendering widget, or
+ * none if it is null.
+ */
+ void setSubtitleWidget(RenderingWidget subtitleWidget);
+
+ /**
+ * Anchors provide the looper on which all track visibility changes
+ * (track.show/hide, setSubtitleWidget) will take place.
+ */
+ Looper getSubtitleLooper();
+ }
+
+ private Anchor mAnchor;
+
+ /**
+ * called from anchor's looper (if any, both when unsetting and
+ * setting)
+ */
+ public void setAnchor(Anchor anchor) {
+ if (mAnchor == anchor) {
+ return;
+ }
+
+ if (mAnchor != null) {
+ checkAnchorLooper();
+ mAnchor.setSubtitleWidget(null);
+ }
+ mAnchor = anchor;
+ mHandler = null;
+ if (mAnchor != null) {
+ mHandler = new Handler(mAnchor.getSubtitleLooper(), mCallback);
+ checkAnchorLooper();
+ mAnchor.setSubtitleWidget(getRenderingWidget());
+ }
+ }
+
+ private void checkAnchorLooper() {
+ assert mHandler != null : "Should have a looper already";
+ assert Looper.myLooper() == mHandler.getLooper()
+ : "Must be called from the anchor's looper";
+ }
+
+ private void processOnAnchor(Message m) {
+ assert mHandler != null : "Should have a looper already";
+ if (Looper.myLooper() == mHandler.getLooper()) {
+ mHandler.dispatchMessage(m);
+ } else {
+ mHandler.sendMessage(m);
+ }
+ }
+
+ interface Listener {
+ /**
+ * Called when a subtitle track has been selected.
+ *
+ * @param track selected subtitle track or null
+ */
+ void onSubtitleTrackSelected(SubtitleTrack track);
+ }
+
+ private Listener mListener;
+}
diff --git a/media/src/main/java/androidx/media/subtitle/SubtitleTrack.java b/media/src/main/java/androidx/media/subtitle/SubtitleTrack.java
new file mode 100644
index 0000000..30c1316
--- /dev/null
+++ b/media/src/main/java/androidx/media/subtitle/SubtitleTrack.java
@@ -0,0 +1,715 @@
+/*
+ * 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 androidx.media.subtitle;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.graphics.Canvas;
+import android.media.MediaFormat;
+import android.media.MediaPlayer.TrackInfo;
+import android.media.SubtitleData;
+import android.os.Handler;
+import android.util.Log;
+import android.util.LongSparseArray;
+import android.util.Pair;
+
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+// Note: This is forked from android.media.SubtitleTrack since P
+/**
+ * A subtitle track abstract base class that is responsible for parsing and displaying
+ * an instance of a particular type of subtitle.
+ *
+ * @hide
+ */
+@RequiresApi(28)
+@RestrictTo(LIBRARY_GROUP)
+public abstract class SubtitleTrack implements MediaTimeProvider.OnMediaTimeListener {
+ private static final String TAG = "SubtitleTrack";
+ private long mLastUpdateTimeMs;
+ private long mLastTimeMs;
+
+ private Runnable mRunnable;
+
+ private final LongSparseArray<Run> mRunsByEndTime = new LongSparseArray<Run>();
+ private final LongSparseArray<Run> mRunsByID = new LongSparseArray<Run>();
+
+ private CueList mCues;
+ private final ArrayList<Cue> mActiveCues = new ArrayList<Cue>();
+ protected boolean mVisible;
+
+ public boolean DEBUG = false;
+
+ protected Handler mHandler = new Handler();
+
+ private MediaFormat mFormat;
+
+ public SubtitleTrack(MediaFormat format) {
+ mFormat = format;
+ mCues = new CueList();
+ clearActiveCues();
+ mLastTimeMs = -1;
+ }
+
+ public final MediaFormat getFormat() {
+ return mFormat;
+ }
+
+ private long mNextScheduledTimeMs = -1;
+
+ /**
+ * Called when there is input data for the subtitle track.
+ */
+ public void onData(SubtitleData data) {
+ long runID = data.getStartTimeUs() + 1;
+ onData(data.getData(), true /* eos */, runID);
+ setRunDiscardTimeMs(
+ runID,
+ (data.getStartTimeUs() + data.getDurationUs()) / 1000);
+ }
+
+ /**
+ * Called when there is input data for the subtitle track. The
+ * complete subtitle for a track can include multiple whole units
+ * (runs). Each of these units can have multiple sections. The
+ * contents of a run are submitted in sequential order, with eos
+ * indicating the last section of the run. Calls from different
+ * runs must not be intermixed.
+ *
+ * @param data subtitle data byte buffer
+ * @param eos true if this is the last section of the run.
+ * @param runID mostly-unique ID for this run of data. Subtitle cues
+ * with runID of 0 are discarded immediately after
+ * display. Cues with runID of ~0 are discarded
+ * only at the deletion of the track object. Cues
+ * with other runID-s are discarded at the end of the
+ * run, which defaults to the latest timestamp of
+ * any of its cues (with this runID).
+ */
+ protected abstract void onData(byte[] data, boolean eos, long runID);
+
+ /**
+ * Called when adding the subtitle rendering widget to the view hierarchy,
+ * as well as when showing or hiding the subtitle track, or when the video
+ * surface position has changed.
+ *
+ * @return the widget that renders this subtitle track. For most renderers
+ * there should be a single shared instance that is used for all
+ * tracks supported by that renderer, as at most one subtitle track
+ * is visible at one time.
+ */
+ public abstract RenderingWidget getRenderingWidget();
+
+ /**
+ * Called when the active cues have changed, and the contents of the subtitle
+ * view should be updated.
+ */
+ public abstract void updateView(ArrayList<Cue> activeCues);
+
+ protected synchronized void updateActiveCues(boolean rebuild, long timeMs) {
+ // out-of-order times mean seeking or new active cues being added
+ // (during their own timespan)
+ if (rebuild || mLastUpdateTimeMs > timeMs) {
+ clearActiveCues();
+ }
+
+ for (Iterator<Pair<Long, Cue>> it =
+ mCues.entriesBetween(mLastUpdateTimeMs, timeMs).iterator(); it.hasNext(); ) {
+ Pair<Long, Cue> event = it.next();
+ Cue cue = event.second;
+
+ if (cue.mEndTimeMs == event.first) {
+ // remove past cues
+ if (DEBUG) Log.v(TAG, "Removing " + cue);
+ mActiveCues.remove(cue);
+ if (cue.mRunID == 0) {
+ it.remove();
+ }
+ } else if (cue.mStartTimeMs == event.first) {
+ // add new cues
+ // TRICKY: this will happen in start order
+ if (DEBUG) Log.v(TAG, "Adding " + cue);
+ if (cue.mInnerTimesMs != null) {
+ cue.onTime(timeMs);
+ }
+ mActiveCues.add(cue);
+ } else if (cue.mInnerTimesMs != null) {
+ // cue is modified
+ cue.onTime(timeMs);
+ }
+ }
+
+ /* complete any runs */
+ while (mRunsByEndTime.size() > 0 && mRunsByEndTime.keyAt(0) <= timeMs) {
+ removeRunsByEndTimeIndex(0); // removes element
+ }
+ mLastUpdateTimeMs = timeMs;
+ }
+
+ private void removeRunsByEndTimeIndex(int ix) {
+ Run run = mRunsByEndTime.valueAt(ix);
+ while (run != null) {
+ Cue cue = run.mFirstCue;
+ while (cue != null) {
+ mCues.remove(cue);
+ Cue nextCue = cue.mNextInRun;
+ cue.mNextInRun = null;
+ cue = nextCue;
+ }
+ mRunsByID.remove(run.mRunID);
+ Run nextRun = run.mNextRunAtEndTimeMs;
+ run.mPrevRunAtEndTimeMs = null;
+ run.mNextRunAtEndTimeMs = null;
+ run = nextRun;
+ }
+ mRunsByEndTime.removeAt(ix);
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ /* remove all cues (untangle all cross-links) */
+ int size = mRunsByEndTime.size();
+ for (int ix = size - 1; ix >= 0; ix--) {
+ removeRunsByEndTimeIndex(ix);
+ }
+
+ super.finalize();
+ }
+
+ private synchronized void takeTime(long timeMs) {
+ mLastTimeMs = timeMs;
+ }
+
+ protected synchronized void clearActiveCues() {
+ if (DEBUG) Log.v(TAG, "Clearing " + mActiveCues.size() + " active cues");
+ mActiveCues.clear();
+ mLastUpdateTimeMs = -1;
+ }
+
+ protected void scheduleTimedEvents() {
+ /* get times for the next event */
+ if (mTimeProvider != null) {
+ mNextScheduledTimeMs = mCues.nextTimeAfter(mLastTimeMs);
+ if (DEBUG) Log.d(TAG, "sched @" + mNextScheduledTimeMs + " after " + mLastTimeMs);
+ mTimeProvider.notifyAt(mNextScheduledTimeMs >= 0
+ ? (mNextScheduledTimeMs * 1000) : MediaTimeProvider.NO_TIME, this);
+ }
+ }
+
+ @Override
+ public void onTimedEvent(long timeUs) {
+ if (DEBUG) Log.d(TAG, "onTimedEvent " + timeUs);
+ synchronized (this) {
+ long timeMs = timeUs / 1000;
+ updateActiveCues(false, timeMs);
+ takeTime(timeMs);
+ }
+ updateView(mActiveCues);
+ scheduleTimedEvents();
+ }
+
+ @Override
+ public void onSeek(long timeUs) {
+ if (DEBUG) Log.d(TAG, "onSeek " + timeUs);
+ synchronized (this) {
+ long timeMs = timeUs / 1000;
+ updateActiveCues(true, timeMs);
+ takeTime(timeMs);
+ }
+ updateView(mActiveCues);
+ scheduleTimedEvents();
+ }
+
+ @Override
+ public void onStop() {
+ synchronized (this) {
+ if (DEBUG) Log.d(TAG, "onStop");
+ clearActiveCues();
+ mLastTimeMs = -1;
+ }
+ updateView(mActiveCues);
+ mNextScheduledTimeMs = -1;
+ mTimeProvider.notifyAt(MediaTimeProvider.NO_TIME, this);
+ }
+
+ protected MediaTimeProvider mTimeProvider;
+
+ /**
+ * Shows subtitle rendering widget
+ */
+ public void show() {
+ if (mVisible) {
+ return;
+ }
+
+ mVisible = true;
+ RenderingWidget renderingWidget = getRenderingWidget();
+ if (renderingWidget != null) {
+ renderingWidget.setVisible(true);
+ }
+ if (mTimeProvider != null) {
+ mTimeProvider.scheduleUpdate(this);
+ }
+ }
+
+ /**
+ * Hides subtitle rendering widget
+ */
+ public void hide() {
+ if (!mVisible) {
+ return;
+ }
+
+ if (mTimeProvider != null) {
+ mTimeProvider.cancelNotifications(this);
+ }
+ RenderingWidget renderingWidget = getRenderingWidget();
+ if (renderingWidget != null) {
+ renderingWidget.setVisible(false);
+ }
+ mVisible = false;
+ }
+
+ protected synchronized boolean addCue(Cue cue) {
+ mCues.add(cue);
+
+ if (cue.mRunID != 0) {
+ Run run = mRunsByID.get(cue.mRunID);
+ if (run == null) {
+ run = new Run();
+ mRunsByID.put(cue.mRunID, run);
+ run.mEndTimeMs = cue.mEndTimeMs;
+ } else if (run.mEndTimeMs < cue.mEndTimeMs) {
+ run.mEndTimeMs = cue.mEndTimeMs;
+ }
+
+ // link-up cues in the same run
+ cue.mNextInRun = run.mFirstCue;
+ run.mFirstCue = cue;
+ }
+
+ // if a cue is added that should be visible, need to refresh view
+ long nowMs = -1;
+ if (mTimeProvider != null) {
+ try {
+ nowMs = mTimeProvider.getCurrentTimeUs(
+ false /* precise */, true /* monotonic */) / 1000;
+ } catch (IllegalStateException e) {
+ // handle as it we are not playing
+ }
+ }
+
+ if (DEBUG) {
+ Log.v(TAG, "mVisible=" + mVisible + ", "
+ + cue.mStartTimeMs + " <= " + nowMs + ", "
+ + cue.mEndTimeMs + " >= " + mLastTimeMs);
+ }
+
+ if (mVisible && cue.mStartTimeMs <= nowMs
+ // we don't trust nowMs, so check any cue since last callback
+ && cue.mEndTimeMs >= mLastTimeMs) {
+ if (mRunnable != null) {
+ mHandler.removeCallbacks(mRunnable);
+ }
+ final SubtitleTrack track = this;
+ final long thenMs = nowMs;
+ mRunnable = new Runnable() {
+ @Override
+ public void run() {
+ // even with synchronized, it is possible that we are going
+ // to do multiple updates as the runnable could be already
+ // running.
+ synchronized (track) {
+ mRunnable = null;
+ updateActiveCues(true, thenMs);
+ updateView(mActiveCues);
+ }
+ }
+ };
+ // delay update so we don't update view on every cue. TODO why 10?
+ if (mHandler.postDelayed(mRunnable, 10 /* delay */)) {
+ if (DEBUG) Log.v(TAG, "scheduling update");
+ } else {
+ if (DEBUG) Log.w(TAG, "failed to schedule subtitle view update");
+ }
+ return true;
+ }
+
+ if (mVisible && cue.mEndTimeMs >= mLastTimeMs
+ && (cue.mStartTimeMs < mNextScheduledTimeMs || mNextScheduledTimeMs < 0)) {
+ scheduleTimedEvents();
+ }
+
+ return false;
+ }
+
+ /**
+ * Sets MediaTimeProvider
+ */
+ public synchronized void setTimeProvider(MediaTimeProvider timeProvider) {
+ if (mTimeProvider == timeProvider) {
+ return;
+ }
+ if (mTimeProvider != null) {
+ mTimeProvider.cancelNotifications(this);
+ }
+ mTimeProvider = timeProvider;
+ if (mTimeProvider != null) {
+ mTimeProvider.scheduleUpdate(this);
+ }
+ }
+
+
+ static class CueList {
+ private static final String TAG = "CueList";
+ // simplistic, inefficient implementation
+ private SortedMap<Long, ArrayList<Cue>> mCues;
+ public boolean DEBUG = false;
+
+ private boolean addEvent(Cue cue, long timeMs) {
+ ArrayList<Cue> cues = mCues.get(timeMs);
+ if (cues == null) {
+ cues = new ArrayList<Cue>(2);
+ mCues.put(timeMs, cues);
+ } else if (cues.contains(cue)) {
+ // do not duplicate cues
+ return false;
+ }
+
+ cues.add(cue);
+ return true;
+ }
+
+ private void removeEvent(Cue cue, long timeMs) {
+ ArrayList<Cue> cues = mCues.get(timeMs);
+ if (cues != null) {
+ cues.remove(cue);
+ if (cues.size() == 0) {
+ mCues.remove(timeMs);
+ }
+ }
+ }
+
+ public void add(Cue cue) {
+ // ignore non-positive-duration cues
+ if (cue.mStartTimeMs >= cue.mEndTimeMs) return;
+
+ if (!addEvent(cue, cue.mStartTimeMs)) {
+ return;
+ }
+
+ long lastTimeMs = cue.mStartTimeMs;
+ if (cue.mInnerTimesMs != null) {
+ for (long timeMs: cue.mInnerTimesMs) {
+ if (timeMs > lastTimeMs && timeMs < cue.mEndTimeMs) {
+ addEvent(cue, timeMs);
+ lastTimeMs = timeMs;
+ }
+ }
+ }
+
+ addEvent(cue, cue.mEndTimeMs);
+ }
+
+ public void remove(Cue cue) {
+ removeEvent(cue, cue.mStartTimeMs);
+ if (cue.mInnerTimesMs != null) {
+ for (long timeMs: cue.mInnerTimesMs) {
+ removeEvent(cue, timeMs);
+ }
+ }
+ removeEvent(cue, cue.mEndTimeMs);
+ }
+
+ public Iterable<Pair<Long, Cue>> entriesBetween(
+ final long lastTimeMs, final long timeMs) {
+ return new Iterable<Pair<Long, Cue>>() {
+ @Override
+ public Iterator<Pair<Long, Cue>> iterator() {
+ if (DEBUG) Log.d(TAG, "slice (" + lastTimeMs + ", " + timeMs + "]=");
+ try {
+ return new EntryIterator(
+ mCues.subMap(lastTimeMs + 1, timeMs + 1));
+ } catch (IllegalArgumentException e) {
+ return new EntryIterator(null);
+ }
+ }
+ };
+ }
+
+ public long nextTimeAfter(long timeMs) {
+ SortedMap<Long, ArrayList<Cue>> tail = null;
+ try {
+ tail = mCues.tailMap(timeMs + 1);
+ if (tail != null) {
+ return tail.firstKey();
+ } else {
+ return -1;
+ }
+ } catch (IllegalArgumentException e) {
+ return -1;
+ } catch (NoSuchElementException e) {
+ return -1;
+ }
+ }
+
+ class EntryIterator implements Iterator<Pair<Long, Cue>> {
+ @Override
+ public boolean hasNext() {
+ return !mDone;
+ }
+
+ @Override
+ public Pair<Long, Cue> next() {
+ if (mDone) {
+ throw new NoSuchElementException("");
+ }
+ mLastEntry = new Pair<Long, Cue>(
+ mCurrentTimeMs, mListIterator.next());
+ mLastListIterator = mListIterator;
+ if (!mListIterator.hasNext()) {
+ nextKey();
+ }
+ return mLastEntry;
+ }
+
+ @Override
+ public void remove() {
+ // only allow removing end tags
+ if (mLastListIterator == null
+ || mLastEntry.second.mEndTimeMs != mLastEntry.first) {
+ throw new IllegalStateException("");
+ }
+
+ // remove end-cue
+ mLastListIterator.remove();
+ mLastListIterator = null;
+ if (mCues.get(mLastEntry.first).size() == 0) {
+ mCues.remove(mLastEntry.first);
+ }
+
+ // remove rest of the cues
+ Cue cue = mLastEntry.second;
+ removeEvent(cue, cue.mStartTimeMs);
+ if (cue.mInnerTimesMs != null) {
+ for (long timeMs: cue.mInnerTimesMs) {
+ removeEvent(cue, timeMs);
+ }
+ }
+ }
+
+ EntryIterator(SortedMap<Long, ArrayList<Cue>> cues) {
+ if (DEBUG) Log.v(TAG, cues + "");
+ mRemainingCues = cues;
+ mLastListIterator = null;
+ nextKey();
+ }
+
+ private void nextKey() {
+ do {
+ try {
+ if (mRemainingCues == null) {
+ throw new NoSuchElementException("");
+ }
+ mCurrentTimeMs = mRemainingCues.firstKey();
+ mListIterator =
+ mRemainingCues.get(mCurrentTimeMs).iterator();
+ try {
+ mRemainingCues =
+ mRemainingCues.tailMap(mCurrentTimeMs + 1);
+ } catch (IllegalArgumentException e) {
+ mRemainingCues = null;
+ }
+ mDone = false;
+ } catch (NoSuchElementException e) {
+ mDone = true;
+ mRemainingCues = null;
+ mListIterator = null;
+ return;
+ }
+ } while (!mListIterator.hasNext());
+ }
+
+ private long mCurrentTimeMs;
+ private Iterator<Cue> mListIterator;
+ private boolean mDone;
+ private SortedMap<Long, ArrayList<Cue>> mRemainingCues;
+ private Iterator<Cue> mLastListIterator;
+ private Pair<Long, Cue> mLastEntry;
+ }
+
+ CueList() {
+ mCues = new TreeMap<Long, ArrayList<Cue>>();
+ }
+ }
+
+ static class Cue {
+ public long mStartTimeMs;
+ public long mEndTimeMs;
+ public long[] mInnerTimesMs;
+ public long mRunID;
+
+ public Cue mNextInRun;
+
+ /**
+ * Called to inform current timeMs to the cue
+ */
+ public void onTime(long timeMs) { }
+ }
+
+ /** update mRunsByEndTime (with default end time) */
+ protected void finishedRun(long runID) {
+ if (runID != 0 && runID != ~0) {
+ Run run = mRunsByID.get(runID);
+ if (run != null) {
+ run.storeByEndTimeMs(mRunsByEndTime);
+ }
+ }
+ }
+
+ /** update mRunsByEndTime with given end time */
+ public void setRunDiscardTimeMs(long runID, long timeMs) {
+ if (runID != 0 && runID != ~0) {
+ Run run = mRunsByID.get(runID);
+ if (run != null) {
+ run.mEndTimeMs = timeMs;
+ run.storeByEndTimeMs(mRunsByEndTime);
+ }
+ }
+ }
+
+ /** whether this is a text track who fires events instead getting rendered */
+ public int getTrackType() {
+ return getRenderingWidget() == null
+ ? TrackInfo.MEDIA_TRACK_TYPE_TIMEDTEXT
+ : TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE;
+ }
+
+
+ private static class Run {
+ public Cue mFirstCue;
+ public Run mNextRunAtEndTimeMs;
+ public Run mPrevRunAtEndTimeMs;
+ public long mEndTimeMs = -1;
+ public long mRunID = 0;
+ private long mStoredEndTimeMs = -1;
+
+ public void storeByEndTimeMs(LongSparseArray<Run> runsByEndTime) {
+ // remove old value if any
+ int ix = runsByEndTime.indexOfKey(mStoredEndTimeMs);
+ if (ix >= 0) {
+ if (mPrevRunAtEndTimeMs == null) {
+ assert (this == runsByEndTime.valueAt(ix));
+ if (mNextRunAtEndTimeMs == null) {
+ runsByEndTime.removeAt(ix);
+ } else {
+ runsByEndTime.setValueAt(ix, mNextRunAtEndTimeMs);
+ }
+ }
+ removeAtEndTimeMs();
+ }
+
+ // add new value
+ if (mEndTimeMs >= 0) {
+ mPrevRunAtEndTimeMs = null;
+ mNextRunAtEndTimeMs = runsByEndTime.get(mEndTimeMs);
+ if (mNextRunAtEndTimeMs != null) {
+ mNextRunAtEndTimeMs.mPrevRunAtEndTimeMs = this;
+ }
+ runsByEndTime.put(mEndTimeMs, this);
+ mStoredEndTimeMs = mEndTimeMs;
+ }
+ }
+
+ public void removeAtEndTimeMs() {
+ Run prev = mPrevRunAtEndTimeMs;
+
+ if (mPrevRunAtEndTimeMs != null) {
+ mPrevRunAtEndTimeMs.mNextRunAtEndTimeMs = mNextRunAtEndTimeMs;
+ mPrevRunAtEndTimeMs = null;
+ }
+ if (mNextRunAtEndTimeMs != null) {
+ mNextRunAtEndTimeMs.mPrevRunAtEndTimeMs = prev;
+ mNextRunAtEndTimeMs = null;
+ }
+ }
+ }
+
+ /**
+ * Interface for rendering subtitles onto a Canvas.
+ */
+ public interface RenderingWidget {
+ /**
+ * Sets the widget's callback, which is used to send updates when the
+ * rendered data has changed.
+ *
+ * @param callback update callback
+ */
+ void setOnChangedListener(OnChangedListener callback);
+
+ /**
+ * Sets the widget's size.
+ *
+ * @param width width in pixels
+ * @param height height in pixels
+ */
+ void setSize(int width, int height);
+
+ /**
+ * Sets whether the widget should draw subtitles.
+ *
+ * @param visible true if subtitles should be drawn, false otherwise
+ */
+ void setVisible(boolean visible);
+
+ /**
+ * Renders subtitles onto a {@link Canvas}.
+ *
+ * @param c canvas on which to render subtitles
+ */
+ void draw(Canvas c);
+
+ /**
+ * Called when the widget is attached to a window.
+ */
+ void onAttachedToWindow();
+
+ /**
+ * Called when the widget is detached from a window.
+ */
+ void onDetachedFromWindow();
+
+ /**
+ * Callback used to send updates about changes to rendering data.
+ */
+ public interface OnChangedListener {
+ /**
+ * Called when the rendering data has changed.
+ *
+ * @param renderingWidget the widget whose data has changed
+ */
+ void onChanged(RenderingWidget renderingWidget);
+ }
+ }
+}
diff --git a/media/src/main/res/values/colors.xml b/media/src/main/res/values/colors.xml
index e44662a..d210220 100644
--- a/media/src/main/res/values/colors.xml
+++ b/media/src/main/res/values/colors.xml
@@ -19,12 +19,4 @@
<!-- The color of the material notification background for media notifications when no custom
color is specified -->
<color name="notification_material_background_media_default_color">#ff424242</color>
-
- <color name="gray">#808080</color>
- <color name="white">#ffffff</color>
- <color name="white_opacity_70">#B3ffffff</color>
- <color name="black_opacity_70">#B3000000</color>
- <color name="title_bar_gradient_start">#50000000</color>
- <color name="title_bar_gradient_end">#00000000</color>
- <color name="bottom_bar_background">#40202020</color>
</resources>
\ No newline at end of file
diff --git a/media/src/main/res/values/dimens.xml b/media/src/main/res/values/dimens.xml
index 2d7b022..be50378 100644
--- a/media/src/main/res/values/dimens.xml
+++ b/media/src/main/res/values/dimens.xml
@@ -1,11 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright 2018 The Android Open Source Project
+<!--
+ 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
+ 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,
@@ -13,60 +14,13 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-
<resources>
- <!-- Dialog size -->
- <eat-comment />
- <!-- The platform's desired fixed width for a dialog along the major axis
- (the screen is in landscape). This may be either a fraction or a dimension.-->
- <dimen name="mr_dialog_fixed_width_major">320dp</dimen>
- <!-- The platform's desired fixed width for a dialog along the minor axis
- (the screen is in portrait). This may be either a fraction or a dimension.-->
- <dimen name="mr_dialog_fixed_width_minor">320dp</dimen>
+ <!-- Shadow radius for video subtitles. -->
+ <dimen name="subtitle_shadow_radius">2dp</dimen>
- <!-- MediaRouteController's volume group list -->
- <eat-comment />
- <!-- Maximum height of volume group list. -->
- <dimen name="mr_controller_volume_group_list_max_height">288dp</dimen>
- <!-- Height of volume group item. -->
- <dimen name="mr_controller_volume_group_list_item_height">68dp</dimen>
- <!-- Size of an item's icon. -->
- <dimen name="mr_controller_volume_group_list_item_icon_size">24dp</dimen>
+ <!-- Shadow offset for video subtitles. -->
+ <dimen name="subtitle_shadow_offset">2dp</dimen>
- <dimen name="mr_controller_volume_group_list_padding_top">16dp</dimen>
- <!-- Group list expand/collapse animation duration. -->
- <integer name="mr_controller_volume_group_list_animation_duration_ms">400</integer>
- <!-- Group list fade in animation duration. -->
- <integer name="mr_controller_volume_group_list_fade_in_duration_ms">400</integer>
- <!-- Group list fade out animation duration. -->
- <integer name="mr_controller_volume_group_list_fade_out_duration_ms">200</integer>
-
- <dimen name="mcv2_embedded_settings_width">150dp</dimen>
- <dimen name="mcv2_embedded_settings_height">36dp</dimen>
- <dimen name="mcv2_embedded_settings_icon_size">20dp</dimen>
- <dimen name="mcv2_embedded_settings_text_height">18dp</dimen>
- <dimen name="mcv2_embedded_settings_main_text_size">12sp</dimen>
- <dimen name="mcv2_embedded_settings_sub_text_size">10sp</dimen>
- <dimen name="mcv2_full_settings_width">225dp</dimen>
- <dimen name="mcv2_full_settings_height">54dp</dimen>
- <dimen name="mcv2_full_settings_icon_size">30dp</dimen>
- <dimen name="mcv2_full_settings_text_height">27dp</dimen>
- <dimen name="mcv2_full_settings_main_text_size">16sp</dimen>
- <dimen name="mcv2_full_settings_sub_text_size">13sp</dimen>
- <dimen name="mcv2_settings_offset">8dp</dimen>
-
- <dimen name="mcv2_transport_controls_padding">4dp</dimen>
- <dimen name="mcv2_pause_icon_size">36dp</dimen>
- <dimen name="mcv2_full_icon_size">28dp</dimen>
- <dimen name="mcv2_embedded_icon_size">24dp</dimen>
- <dimen name="mcv2_minimal_icon_size">24dp</dimen>
- <dimen name="mcv2_icon_margin">10dp</dimen>
-
- <dimen name="mcv2_full_album_image_portrait_size">232dp</dimen>
- <dimen name="mcv2_full_album_image_landscape_size">176dp</dimen>
-
- <dimen name="mcv2_custom_progress_max_size">2dp</dimen>
- <dimen name="mcv2_custom_progress_thumb_size">12dp</dimen>
- <dimen name="mcv2_buffer_view_height">5dp</dimen>
- <!-- TODO: adjust bottom bar view -->
+ <!-- Outline width for video subtitles. -->
+ <dimen name="subtitle_outline_width">2dp</dimen>
</resources>
diff --git a/media/src/main/res/values/symbols.xml b/media/src/main/res/values/symbols.xml
deleted file mode 100644
index ee0e8c6..0000000
--- a/media/src/main/res/values/symbols.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/* Copyright 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.
-*/
--->
-<resources>
- <!--java-symbol type="id" name="cc" />
- <java-symbol type="id" name="ffwd" />
- <java-symbol type="id" name="mediacontroller_progress" />
- <java-symbol type="id" name="next" />
- <java-symbol type="id" name="pause" />
- <java-symbol type="id" name="prev" />
- <java-symbol type="id" name="rew" />
- <java-symbol type="id" name="time" />
- <java-symbol type="id" name="time_current" /-->
-</resources>
\ No newline at end of file
diff --git a/samples/SupportMediaDemos/OWNERS b/samples/SupportMediaDemos/OWNERS
new file mode 100644
index 0000000..2f5d227
--- /dev/null
+++ b/samples/SupportMediaDemos/OWNERS
@@ -0,0 +1,6 @@
+insun@google.com
+jinpark@google.com
+sungsoo@google.com
+hdmoon@google.com
+jaewan@google.com
+akersten@google.com
\ No newline at end of file
diff --git a/samples/SupportMediaDemos/build.gradle b/samples/SupportMediaDemos/build.gradle
new file mode 100644
index 0000000..fecf2a5
--- /dev/null
+++ b/samples/SupportMediaDemos/build.gradle
@@ -0,0 +1,31 @@
+/*
+ * 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.
+ */
+
+plugins {
+ id("SupportAndroidTestAppPlugin")
+}
+
+dependencies {
+ implementation(project(":media"))
+ implementation(project(":media-widget"))
+}
+
+android {
+ defaultConfig {
+ minSdkVersion = 19
+ targetSdkVersion = 26
+ }
+}
diff --git a/samples/SupportMediaDemos/src/main/AndroidManifest.xml b/samples/SupportMediaDemos/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..5348161
--- /dev/null
+++ b/samples/SupportMediaDemos/src/main/AndroidManifest.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ package="com.example.androidx.media">
+
+ <uses-sdk tools:overrideLibrary="androidx.media.widget" />
+
+ <application android:label="Video View Test">
+
+ <!-- Video Selection Activity -->
+ <activity android:name="VideoSelector"
+ android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
+ android:configChanges="orientation|screenSize"
+ >
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.LAUNCHER"/>
+ </intent-filter>
+ </activity>
+
+ <!-- Video playback activity -->
+ <activity android:name="VideoViewTest"
+ android:configChanges="orientation|screenSize"
+ >
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW"/>
+ </intent-filter>
+ </activity>
+
+ </application>
+ <uses-permission android:name="android.permission.INTERNET" />
+ <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+
+</manifest>
diff --git a/samples/SupportMediaDemos/src/main/java/com/example/androidx/media/VideoSelector.java b/samples/SupportMediaDemos/src/main/java/com/example/androidx/media/VideoSelector.java
new file mode 100644
index 0000000..a77789f
--- /dev/null
+++ b/samples/SupportMediaDemos/src/main/java/com/example/androidx/media/VideoSelector.java
@@ -0,0 +1,304 @@
+/*
+ * 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 com.example.androidx.media;
+
+import android.Manifest;
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.EditText;
+import android.widget.ListView;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.TreeMap;
+
+/**
+ * Start activity for the VideoViewTest application. This class manages the UI
+ * which allows a user to select a video to play back.
+ */
+public class VideoSelector extends Activity {
+ private ListView mSelectList;
+ private VideoItemList mSelectItems;
+ private EditText mUrlText;
+ private CheckBox mTextureViewCheckbox;
+ private CheckBox mLoopingCheckbox;
+ private CheckBox mAdvertisementCheckBox;
+
+ private static final String TEST_VID_STASH = "/sdcard";
+ private static final int EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE = 100;
+
+ private Intent createLaunchIntent(Context ctx, String url) {
+ Intent ret_val = new Intent(ctx, VideoViewTest.class);
+ ret_val.setData(Uri.parse(url));
+ ret_val.putExtra(
+ VideoViewTest.LOOPING_EXTRA_NAME, mLoopingCheckbox.isChecked());
+ ret_val.putExtra(
+ VideoViewTest.USE_TEXTURE_VIEW_EXTRA_NAME, mTextureViewCheckbox.isChecked());
+ ret_val.putExtra(
+ VideoViewTest.MEDIA_TYPE_ADVERTISEMENT, mAdvertisementCheckBox.isChecked());
+ return ret_val;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.video_selector);
+
+ mSelectList = (ListView) findViewById(R.id.select_list);
+ final Button playButton = (Button) findViewById(R.id.play_button);
+ mUrlText = (EditText) findViewById(R.id.video_selection_input);
+ mSelectItems = null;
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ if (checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE)
+ == PackageManager.PERMISSION_GRANTED) {
+ setUpInitialItemList();
+ } else {
+ requestPermissions(new String[] {Manifest.permission.READ_EXTERNAL_STORAGE},
+ EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE);
+ }
+ } else {
+ setUpInitialItemList();
+ }
+
+ playButton.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ Intent launch = createLaunchIntent(
+ VideoSelector.this,
+ mUrlText.getText().toString());
+ startActivity(launch);
+ }
+ });
+ mLoopingCheckbox = findViewById(R.id.looping_checkbox);
+ mLoopingCheckbox.setChecked(false);
+ mTextureViewCheckbox = findViewById(R.id.use_textureview_checkbox);
+ mTextureViewCheckbox.setChecked(false);
+ mAdvertisementCheckBox = findViewById(R.id.media_type_advertisement);
+ mAdvertisementCheckBox.setChecked(false);
+ }
+
+ @Override
+ public void onBackPressed() {
+ if ((null != mSelectItems) && (null != mSelectHandler) && !mSelectItems.getIsRoot()) {
+ mSelectHandler.onItemClick(null, null, 0, 0);
+ } else {
+ finish();
+ }
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] results) {
+ if (requestCode == EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE) {
+ if (results.length > 0 && results[0] == PackageManager.PERMISSION_GRANTED) {
+ setUpInitialItemList();
+ }
+ }
+ }
+
+ private void setUpInitialItemList() {
+ mSelectItems = createVil(TEST_VID_STASH);
+ mSelectList.setAdapter(mSelectItems);
+ mSelectList.setOnItemClickListener(mSelectHandler);
+ }
+
+ /**
+ * VideoItem is a class used to represent a selectable item on the listbox
+ * used to select videos to playback.
+ */
+ private static class VideoItem {
+ private final String mToStringName;
+ private final String mName;
+ private final String mUrl;
+ private final boolean mIsDir;
+
+ VideoItem(String name, String url, boolean isDir) {
+ mName = name;
+ mUrl = url;
+ mIsDir = isDir;
+
+ if (isDir) {
+ mToStringName = String.format("[dir] %s", name);
+ } else {
+ int ndx = url.indexOf(':');
+ if (ndx > 0) {
+ mToStringName = String.format("[%s] %s", url.substring(0, ndx), name);
+ } else {
+ mToStringName = name;
+ }
+ }
+ }
+
+ public static VideoItem createFromLinkFile(File f) {
+ VideoItem retVal = null;
+
+ try {
+ BufferedReader rd = new BufferedReader(new FileReader(f));
+ String name = rd.readLine();
+ String url = rd.readLine();
+ if ((null != name) && (null != url)) {
+ retVal = new VideoItem(name, url, false);
+ }
+ } catch (FileNotFoundException e) {
+ } catch (IOException e) {
+ }
+
+ return retVal;
+ }
+
+ @Override
+ public String toString() {
+ return mToStringName;
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ public String getUrl() {
+ return mUrl;
+ }
+
+ public boolean getIsDir() {
+ return mIsDir;
+ }
+ }
+
+ private OnItemClickListener mSelectHandler = new OnItemClickListener() {
+ public void onItemClick(AdapterView parent,
+ View v,
+ int position,
+ long id) {
+ if ((position >= 0) && (position < mSelectItems.getCount())) {
+ VideoItem item = mSelectItems.getItem(position);
+ if (item.getIsDir()) {
+ VideoItemList new_list = createVil(item.getUrl());
+ if (null != new_list) {
+ mSelectItems = new_list;
+ mSelectList.setAdapter(mSelectItems);
+ }
+ } else {
+ Intent launch = createLaunchIntent(
+ VideoSelector.this,
+ item.getUrl());
+ startActivity(launch);
+ }
+ }
+ }
+ };
+
+ /**
+ * VideoItemList is an array adapter of video items used by the android
+ * framework to populate the list of videos to select.
+ */
+ private class VideoItemList extends ArrayAdapter<VideoItem> {
+ private final String mPath;
+ private final boolean mIsRoot;
+
+ private VideoItemList(String path, boolean isRoot) {
+ super(VideoSelector.this,
+ R.layout.video_list_item,
+ R.id.video_list_item);
+ mPath = path;
+ mIsRoot = isRoot;
+ }
+
+ public String getPath() {
+ return mPath;
+ }
+
+ public boolean getIsRoot() {
+ return mIsRoot;
+ }
+ };
+
+ private VideoItemList createVil(String p) {
+ boolean is_root = TEST_VID_STASH.equals(p);
+
+ File dir = new File(p);
+ if (!dir.isDirectory() || !dir.canRead()) {
+ return null;
+ }
+
+ VideoItemList retVal = new VideoItemList(p, is_root);
+
+ // If this is not the root directory, go ahead and add the back link to
+ // our parent.
+ if (!is_root) {
+ retVal.add(new VideoItem("..", dir.getParentFile().getAbsolutePath(), true));
+ }
+
+ // Make a sorted list of directories and files contained in this
+ // directory.
+ TreeMap<String, VideoItem> dirs = new TreeMap<String, VideoItem>();
+ TreeMap<String, VideoItem> files = new TreeMap<String, VideoItem>();
+
+ File search_dir = new File(p);
+ File[] flist = search_dir.listFiles();
+ if (null == flist) {
+ return retVal;
+ }
+
+ for (File f : flist) {
+ if (f.canRead()) {
+ if (f.isFile()) {
+ String fname = f.getName();
+ VideoItem newItem = null;
+
+ if (fname.endsWith(".url")) {
+ newItem = VideoItem.createFromLinkFile(f);
+ } else {
+ String url = "file://" + f.getAbsolutePath();
+ newItem = new VideoItem(fname, url, false);
+ }
+
+ if (null != newItem) {
+ files.put(newItem.getName(), newItem);
+ }
+ } else if (f.isDirectory()) {
+ VideoItem newItem = new VideoItem(f.getName(), f.getAbsolutePath(), true);
+ dirs.put(newItem.getName(), newItem);
+ }
+ }
+ }
+
+ // now add the the sorted directories to the result set.
+ for (VideoItem vi : dirs.values()) {
+ retVal.add(vi);
+ }
+
+ // finally add the the sorted files to the result set.
+ for (VideoItem vi : files.values()) {
+ retVal.add(vi);
+ }
+
+ return retVal;
+ }
+}
diff --git a/samples/SupportMediaDemos/src/main/java/com/example/androidx/media/VideoViewTest.java b/samples/SupportMediaDemos/src/main/java/com/example/androidx/media/VideoViewTest.java
new file mode 100644
index 0000000..f37033d
--- /dev/null
+++ b/samples/SupportMediaDemos/src/main/java/com/example/androidx/media/VideoViewTest.java
@@ -0,0 +1,301 @@
+/*
+ * 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 com.example.androidx.media;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v4.media.session.MediaControllerCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.Toast;
+
+import androidx.media.widget.MediaControlView2;
+import androidx.media.widget.VideoView2;
+
+/**
+ * Test application for VideoView2/MediaControlView2
+ */
+@SuppressLint("NewApi")
+public class VideoViewTest extends Activity {
+ public static final String LOOPING_EXTRA_NAME =
+ "com.example.androidx.media.VideoViewTest.IsLooping";
+ public static final String USE_TEXTURE_VIEW_EXTRA_NAME =
+ "com.example.androidx.media.VideoViewTest.UseTextureView";
+ public static final String MEDIA_TYPE_ADVERTISEMENT =
+ "com.example.androidx.media.VideoViewTest.MediaTypeAdvertisement";
+ private static final String TAG = "VideoViewTest";
+
+ private MyVideoView mVideoView = null;
+ private float mSpeed = 1.0f;
+
+ private MediaControlView2 mMediaControlView = null;
+
+ private boolean mUseTextureView = false;
+ private int mPrevWidth;
+ private int mPrevHeight;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ //Remove title bar
+ requestWindowFeature(Window.FEATURE_NO_TITLE);
+
+ setContentView(R.layout.video_activity);
+
+ mVideoView = findViewById(R.id.video_view);
+ mVideoView.setActivity(this);
+
+ String errorString = null;
+ Intent intent = getIntent();
+ Uri contentUri;
+ if (intent == null || (contentUri = intent.getData()) == null || !contentUri.isAbsolute()) {
+ errorString = "Invalid intent";
+ } else {
+ mUseTextureView = intent.getBooleanExtra(USE_TEXTURE_VIEW_EXTRA_NAME, false);
+ if (mUseTextureView) {
+ mVideoView.setViewType(VideoView2.VIEW_TYPE_TEXTUREVIEW);
+ }
+
+ mVideoView.setFullScreenRequestListener(new FullScreenRequestListener());
+ mVideoView.setVideoUri(contentUri);
+
+ mMediaControlView = new MediaControlView2(this);
+ mVideoView.setMediaControlView2(mMediaControlView, 2000);
+ }
+ if (errorString != null) {
+ showErrorDialog(errorString);
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ if (mVideoView.isAttachedToWindow()) {
+ mVideoView.getMediaController().getTransportControls().play();
+ mVideoView.getMediaController().registerCallback(mMediaControllerCallback);
+ } else {
+ mVideoView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
+ @Override
+ public void onViewAttachedToWindow(View v) {
+ mVideoView.getMediaController().getTransportControls().play();
+ mVideoView.getMediaController().registerCallback(mMediaControllerCallback);
+ }
+
+ @Override
+ public void onViewDetachedFromWindow(View v) {
+ // No need to remove callback here since MediaSession has already been
+ // destroyed.
+ }
+ });
+ }
+ setTitle(getViewTypeString(mVideoView));
+ }
+
+ @Override
+ protected void onPause() {
+ mVideoView.getMediaController().getTransportControls().pause();
+ getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ super.onPause();
+ }
+
+ @Override
+ protected void onDestroy() {
+ Log.d(TAG, "onDestroy");
+ mVideoView.getMediaController().unregisterCallback(mMediaControllerCallback);
+ mVideoView.getMediaController().getTransportControls().stop();
+ super.onDestroy();
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+ int screenWidth = getResources().getDisplayMetrics().widthPixels;
+ if (ev.getRawX() < (screenWidth / 2.0f)) {
+ // TODO: getSpeed() not needed?
+ mSpeed -= 0.1f;
+ } else {
+ mSpeed += 0.1f;
+ }
+ mVideoView.setSpeed(mSpeed);
+ Toast.makeText(this, "speed rate: " + String.format("%.2f", mSpeed), Toast.LENGTH_SHORT)
+ .show();
+ }
+ return super.onTouchEvent(ev);
+ }
+
+ private void showErrorDialog(String errorMessage) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle("Playback error")
+ .setMessage(errorMessage)
+ .setCancelable(false)
+ .setPositiveButton("OK",
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ finish();
+ }
+ }).show();
+ }
+
+ MediaControllerCompat.Callback mMediaControllerCallback = new MediaControllerCompat.Callback() {
+ @Override
+ public void onPlaybackStateChanged(PlaybackStateCompat state) {
+ switch (state.getState()) {
+ case PlaybackStateCompat.STATE_STOPPED:
+ getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ break;
+ case PlaybackStateCompat.STATE_ERROR:
+ showErrorDialog("Error: (" + state.getErrorMessage() + ")");
+ break;
+ }
+ }
+ };
+
+ private class FullScreenRequestListener implements VideoView2.OnFullScreenRequestListener {
+ @Override
+ public void onFullScreenRequest(View view, boolean fullScreen) {
+ // TODO: Remove bottom controls after adding back button functionality.
+ if (mPrevHeight == 0 && mPrevWidth == 0) {
+ ViewGroup.LayoutParams params = mVideoView.getLayoutParams();
+ mPrevWidth = params.width;
+ mPrevHeight = params.height;
+ }
+
+ if (fullScreen) {
+ // Remove notification bar
+ getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
+ WindowManager.LayoutParams.FLAG_FULLSCREEN);
+
+ ViewGroup.LayoutParams params = mVideoView.getLayoutParams();
+ params.width = ViewGroup.LayoutParams.MATCH_PARENT;
+ params.height = ViewGroup.LayoutParams.MATCH_PARENT;
+ mVideoView.setLayoutParams(params);
+ } else {
+ // Restore notification bar
+ getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
+
+ ViewGroup.LayoutParams params = mVideoView.getLayoutParams();
+ params.width = mPrevWidth;
+ params.height = mPrevHeight;
+ mVideoView.setLayoutParams(params);
+ }
+ mVideoView.requestLayout();
+ mVideoView.invalidate();
+ }
+ }
+
+ /**
+ * Extension of the stock android video view used to hook and override
+ * keypress behavior. Mainly used to make sure that certain keystrokes
+ * don't automatically bring up the andriod MediaController widget (which
+ * then steals focus)
+ *
+ * @author johngro@google.com (John Grossman)
+ */
+ public static class MyVideoView extends VideoView2 {
+ private float mDX;
+ private float mDY;
+ private Activity mActivity;
+
+ public MyVideoView(Context context) {
+ super(context);
+ }
+
+ public MyVideoView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public MyVideoView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ switch (ev.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ mDX = ev.getRawX() - getX();
+ mDY = ev.getRawY() - getY();
+ super.onTouchEvent(ev);
+ return true;
+ case MotionEvent.ACTION_MOVE:
+ animate()
+ .x(ev.getRawX() - mDX)
+ .y(ev.getRawY() - mDY)
+ .setDuration(0)
+ .start();
+ super.onTouchEvent(ev);
+ return true;
+ }
+ return super.onTouchEvent(ev);
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_DOWN) {
+ mActivity.finish();
+ }
+ return true;
+ }
+
+ public void setActivity(Activity activity) {
+ mActivity = activity;
+ }
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ if (mVideoView.getViewType() == VideoView2.VIEW_TYPE_SURFACEVIEW) {
+ mVideoView.setViewType(VideoView2.VIEW_TYPE_TEXTUREVIEW);
+ Toast.makeText(this, "switch to TextureView", Toast.LENGTH_SHORT).show();
+ setTitle(getViewTypeString(mVideoView));
+ } else if (mVideoView.getViewType() == VideoView2.VIEW_TYPE_TEXTUREVIEW) {
+ mVideoView.setViewType(VideoView2.VIEW_TYPE_SURFACEVIEW);
+ Toast.makeText(this, "switch to SurfaceView", Toast.LENGTH_SHORT).show();
+ setTitle(getViewTypeString(mVideoView));
+ }
+ }
+
+ private String getViewTypeString(VideoView2 videoView) {
+ if (videoView == null) {
+ return "Unknown";
+ }
+ int type = videoView.getViewType();
+ if (type == VideoView2.VIEW_TYPE_SURFACEVIEW) {
+ return "SurfaceView";
+ } else if (type == VideoView2.VIEW_TYPE_TEXTUREVIEW) {
+ return "TextureView";
+ }
+ return "Unknown";
+ }
+}
diff --git a/samples/SupportMediaDemos/src/main/res/layout/video_activity.xml b/samples/SupportMediaDemos/src/main/res/layout/video_activity.xml
new file mode 100644
index 0000000..94784d4
--- /dev/null
+++ b/samples/SupportMediaDemos/src/main/res/layout/video_activity.xml
@@ -0,0 +1,141 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:widget="http://schemas.android.com/apk/com.android.media.update"
+ android:id="@+id/video_player_main"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="#3F51B5">
+
+ <!-- MediaControlView will be created manually in code -->
+ <view
+ android:id="@+id/video_view"
+ class="com.example.androidx.media.VideoViewTest$MyVideoView"
+ android:layout_width="250dp"
+ android:layout_height="250dp"
+ android:layout_centerInParent="true"
+ widget:enableControlView="false" />
+
+ <!-- Video Status -->
+ <TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/vid_status"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentTop="true"
+ android:visibility="invisible">
+
+ <TableRow>
+
+ <TextView
+ style="@style/status_tag"
+ android:text="@string/vid_status_codec_tag" />
+
+ <TextView
+ android:id="@+id/vid_codec"
+ style="@style/status_value" />
+ </TableRow>
+
+ <TableRow>
+
+ <TextView
+ style="@style/status_tag"
+ android:text="@string/vid_status_bitrate_tag" />
+
+ <TextView
+ android:id="@+id/vid_bitrate"
+ style="@style/status_value" />
+ </TableRow>
+
+ <TableRow>
+
+ <TextView
+ style="@style/status_tag"
+ android:text="@string/vid_status_mode_tag" />
+
+ <TextView
+ android:id="@+id/vid_mode"
+ style="@style/status_value" />
+ </TableRow>
+
+ <TableRow>
+
+ <TextView
+ style="@style/status_tag"
+ android:text="@string/vid_status_outrect_tag" />
+
+ <TextView
+ android:id="@+id/vid_outrect"
+ style="@style/status_value" />
+ </TableRow>
+
+ <TableRow>
+
+ <TextView
+ style="@style/status_tag"
+ android:text="@string/vid_status_stereo_mode_tag" />
+
+ <TextView
+ android:id="@+id/vid_stereo_mode"
+ style="@style/status_value" />
+ </TableRow>
+ </TableLayout>
+
+ <!-- Audio Status -->
+ <TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/aud_status"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:layout_alignParentTop="true"
+ android:visibility="invisible">
+
+ <TableRow>
+
+ <TextView
+ style="@style/status_tag"
+ android:text="@string/aud_status_codec_tag" />
+
+ <TextView
+ android:id="@+id/aud_codec"
+ style="@style/status_value" />
+ </TableRow>
+
+ <TableRow>
+
+ <TextView
+ style="@style/status_tag"
+ android:text="@string/aud_status_bitrate_tag" />
+
+ <TextView
+ android:id="@+id/aud_bitrate"
+ style="@style/status_value" />
+ </TableRow>
+
+ <TableRow>
+
+ <TextView
+ style="@style/status_tag"
+ android:text="@string/aud_status_mode_tag" />
+
+ <TextView
+ android:id="@+id/aud_mode"
+ style="@style/status_value" />
+ </TableRow>
+ </TableLayout>
+</RelativeLayout>
diff --git a/samples/SupportMediaDemos/src/main/res/layout/video_list_item.xml b/samples/SupportMediaDemos/src/main/res/layout/video_list_item.xml
new file mode 100644
index 0000000..c4e6045
--- /dev/null
+++ b/samples/SupportMediaDemos/src/main/res/layout/video_list_item.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<TextView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/video_list_item"
+ style="@style/video_list_item_text"
+ android:padding="12sp"
+/>
diff --git a/samples/SupportMediaDemos/src/main/res/layout/video_selector.xml b/samples/SupportMediaDemos/src/main/res/layout/video_selector.xml
new file mode 100644
index 0000000..485090f
--- /dev/null
+++ b/samples/SupportMediaDemos/src/main/res/layout/video_selector.xml
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/video_selector_main"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:orientation="vertical"
+ android:padding="16sp">
+
+ <TextView
+ android:id="@+id/title_text"
+ style="@style/title_text"
+ android:layout_alignParentTop="true"
+ android:text="@string/select_video_prompt" />
+
+ <CheckBox
+ android:id="@+id/looping_checkbox"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:layout_below="@id/title_text"
+ android:text="@string/looping_text" />
+
+ <CheckBox
+ android:id="@+id/use_textureview_checkbox"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/title_text"
+ android:layout_toLeftOf="@id/looping_checkbox"
+ android:text="@string/texture_view_text" />
+
+ <CheckBox
+ android:id="@+id/media_type_advertisement"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/title_text"
+ android:layout_toLeftOf="@id/use_textureview_checkbox"
+ android:text="@string/advertisement_text" />
+
+ <Button
+ android:id="@+id/play_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentRight="true"
+ android:text="@string/play_button_text" />
+
+ <EditText
+ android:id="@+id/video_selection_input"
+ android:layout_width="400sp"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentLeft="true"
+ android:layout_toLeftOf="@id/play_button"
+ android:editable="true" />
+
+ <ListView
+ android:id="@+id/select_list"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_above="@id/video_selection_input"
+ android:layout_below="@id/looping_checkbox" />
+</RelativeLayout>
diff --git a/samples/SupportMediaDemos/src/main/res/values/strings.xml b/samples/SupportMediaDemos/src/main/res/values/strings.xml
new file mode 100644
index 0000000..ba49f42
--- /dev/null
+++ b/samples/SupportMediaDemos/src/main/res/values/strings.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="vid_status_codec_tag">Video Codec:</string>
+ <string name="vid_status_bitrate_tag">Avg. Bitrate:</string>
+ <string name="vid_status_mode_tag">Video Mode:</string>
+ <string name="vid_status_outrect_tag">Dest Rect:</string>
+ <string name="aud_status_codec_tag">Audio Codec:</string>
+ <string name="vid_status_stereo_mode_tag">Stereo Mode:</string>
+ <string name="aud_status_bitrate_tag">Bitrate:</string>
+ <string name="aud_status_mode_tag">Audio Mode:</string>
+ <string name="buf_status_fmt_string">Buffering: %d%%</string>
+ <string name="select_video_prompt">Select Video</string>
+ <string name="select_button_text">Select</string>
+ <string name="play_button_text">Play</string>
+ <string name="looping_text">Looping</string>
+ <string name="texture_view_text">Use TextureView</string>
+ <string name="advertisement_text">Advertisement</string>
+</resources>
diff --git a/samples/SupportMediaDemos/src/main/res/values/style.xml b/samples/SupportMediaDemos/src/main/res/values/style.xml
new file mode 100644
index 0000000..27ddd23
--- /dev/null
+++ b/samples/SupportMediaDemos/src/main/res/values/style.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<resources>
+ <style name="basic_text">
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ </style>
+
+ <style name="title_text" parent="@style/basic_text">
+ <item name="android:textSize">24sp</item>
+ </style>
+
+ <style name="video_list_item_text" parent="@style/basic_text">
+ <item name="android:textSize">24sp</item>
+ </style>
+
+ <style name="status_text" parent="@style/basic_text">
+ <item name="android:textSize">24sp</item>
+ </style>
+ <style name="status_value" parent="@style/status_text">
+ <item name="android:gravity">left</item>
+ </style>
+ <style name="status_tag" parent="@style/status_text">
+ <item name="android:paddingRight">5dp</item>
+ <item name="android:gravity">right</item>
+ </style>
+</resources>
+
diff --git a/samples/SupportSliceDemos/src/main/AndroidManifest.xml b/samples/SupportSliceDemos/src/main/AndroidManifest.xml
index 355a77a..4d43211 100644
--- a/samples/SupportSliceDemos/src/main/AndroidManifest.xml
+++ b/samples/SupportSliceDemos/src/main/AndroidManifest.xml
@@ -48,6 +48,7 @@
<provider android:authorities="com.example.androidx.slice.demos"
android:name=".SampleSliceProvider"
+ android:exported="true"
android:grantUriPermissions="true">
<intent-filter>
<action android:name="androidx.intent.SLICE_ACTION"/>
diff --git a/samples/SupportSliceDemos/src/main/java/com/example/androidx/slice/demos/SliceBrowser.java b/samples/SupportSliceDemos/src/main/java/com/example/androidx/slice/demos/SliceBrowser.java
index f1e2976..622f047 100644
--- a/samples/SupportSliceDemos/src/main/java/com/example/androidx/slice/demos/SliceBrowser.java
+++ b/samples/SupportSliceDemos/src/main/java/com/example/androidx/slice/demos/SliceBrowser.java
@@ -24,7 +24,6 @@
import android.content.ContentResolver;
import android.content.Intent;
import android.content.pm.ActivityInfo;
-import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.database.Cursor;
@@ -133,7 +132,6 @@
mSearchView.setQuery(savedInstanceState.getString("SELECTED_QUERY"), true);
}
- grantPackage(getPackageName());
// TODO: Listen for changes.
updateAvailableSlices();
if (TEST_INTENT) {
@@ -149,7 +147,6 @@
mTypeMenu.add("Shortcut");
mTypeMenu.add("Small");
mTypeMenu.add("Large");
- menu.add("Auth");
super.onCreateOptionsMenu(menu);
return true;
}
@@ -157,9 +154,6 @@
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getTitle().toString()) {
- case "Auth":
- authAllSlices();
- return true;
case "Shortcut":
mTypeMenu.setIcon(R.drawable.ic_shortcut);
mSelectedMode = SliceView.MODE_SHORTCUT;
@@ -186,21 +180,6 @@
outState.putString("SELECTED_QUERY", mSearchView.getQuery().toString());
}
- private void authAllSlices() {
- List<ApplicationInfo> packages = getPackageManager().getInstalledApplications(0);
- for (ApplicationInfo info : packages) {
- grantPackage(info.packageName);
- }
- }
-
- private void grantPackage(String packageName) {
- for (int i = 0; i < URI_PATHS.length; i++) {
- grantUriPermission(packageName, getUri(URI_PATHS[i], getApplicationContext()),
- Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
- | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
- }
- }
-
private void updateAvailableSlices() {
mSliceUris.clear();
List<PackageInfo> packageInfos = getPackageManager()
diff --git a/settings.gradle b/settings.gradle
index 16e5be7..d969d88 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -68,6 +68,7 @@
includeProject(":loader", "loader")
includeProject(":localbroadcastmanager", "localbroadcastmanager")
includeProject(":media", "media")
+includeProject(":media-widget", "media-widget")
includeProject(":mediarouter", "mediarouter")
includeProject(":palette", "palette")
includeProject(":palette-ktx", "palette/ktx")
@@ -122,6 +123,7 @@
includeProject(":support-emoji-demos", new File(samplesRoot, "SupportEmojiDemos"))
includeProject(":support-leanback-demos", new File(samplesRoot, "SupportLeanbackDemos"))
//includeProject(":support-leanback-jank", new File(samplesRoot, "SupportLeanbackJank"))
+includeProject(":support-media-demos", new File(samplesRoot, "SupportMediaDemos"))
includeProject(":support-percent-demos", new File(samplesRoot, "SupportPercentDemos"))
includeProject(":support-preference-demos", new File(samplesRoot, "SupportPreferenceDemos"))
includeProject(":support-slices-demos", new File(samplesRoot, "SupportSliceDemos"))
diff --git a/slices/core/src/androidTest/java/androidx/slice/SliceTest.java b/slices/core/src/androidTest/java/androidx/slice/SliceTest.java
index 041c8fe..e092c97 100644
--- a/slices/core/src/androidTest/java/androidx/slice/SliceTest.java
+++ b/slices/core/src/androidTest/java/androidx/slice/SliceTest.java
@@ -23,9 +23,9 @@
import static android.app.slice.SliceItem.FORMAT_ACTION;
import static android.app.slice.SliceItem.FORMAT_IMAGE;
import static android.app.slice.SliceItem.FORMAT_INT;
+import static android.app.slice.SliceItem.FORMAT_LONG;
import static android.app.slice.SliceItem.FORMAT_SLICE;
import static android.app.slice.SliceItem.FORMAT_TEXT;
-import static android.app.slice.SliceItem.FORMAT_TIMESTAMP;
import static android.app.slice.SliceProvider.SLICE_TYPE;
import static org.junit.Assert.assertEquals;
@@ -184,7 +184,7 @@
assertEquals(1, s.getItems().size());
SliceItem item = s.getItems().get(0);
- assertEquals(FORMAT_TIMESTAMP, item.getFormat());
+ assertEquals(FORMAT_LONG, item.getFormat());
assertEquals(43, item.getTimestamp());
}
diff --git a/slices/core/src/main/java/androidx/slice/Slice.java b/slices/core/src/main/java/androidx/slice/Slice.java
index a2c3325..aa077cb 100644
--- a/slices/core/src/main/java/androidx/slice/Slice.java
+++ b/slices/core/src/main/java/androidx/slice/Slice.java
@@ -35,7 +35,6 @@
import static android.app.slice.SliceItem.FORMAT_REMOTE_INPUT;
import static android.app.slice.SliceItem.FORMAT_SLICE;
import static android.app.slice.SliceItem.FORMAT_TEXT;
-import static android.app.slice.SliceItem.FORMAT_TIMESTAMP;
import static androidx.slice.SliceConvert.unwrap;
import static androidx.slice.core.SliceHints.HINT_KEYWORDS;
@@ -388,7 +387,7 @@
}
/**
- * Add a timestamp to the slice being constructed
+ * Add a long to the slice being constructed
* @param subType Optional template-specific type information
* @see {@link SliceItem#getSubType()}
*/
@@ -399,6 +398,16 @@
}
/**
+ * Add a long to the slice being constructed
+ * @param subType Optional template-specific type information
+ * @see {@link SliceItem#getSubType()}
+ */
+ public Slice.Builder addLong(long time, @Nullable String subType,
+ @SliceHint List<String> hints) {
+ return addLong(time, subType, hints.toArray(new String[hints.size()]));
+ }
+
+ /**
* Add a timestamp to the slice being constructed
* @param subType Optional template-specific type information
* @see {@link SliceItem#getSubType()}
@@ -407,7 +416,7 @@
@Deprecated
public Slice.Builder addTimestamp(long time, @Nullable String subType,
@SliceHint String... hints) {
- mItems.add(new SliceItem(time, FORMAT_TIMESTAMP, subType, hints));
+ mItems.add(new SliceItem(time, FORMAT_LONG, subType, hints));
return this;
}
@@ -473,7 +482,7 @@
*/
@RestrictTo(Scope.LIBRARY)
public static void addHints(StringBuilder sb, String[] hints) {
- if (hints.length == 0) return;
+ if (hints == null || hints.length == 0) return;
sb.append("(");
int end = hints.length - 1;
@@ -510,6 +519,6 @@
private static Slice callBindSlice(Context context, Uri uri,
Set<SliceSpec> supportedSpecs) {
return SliceConvert.wrap(context.getSystemService(SliceManager.class)
- .bindSlice(uri, new ArrayList<>(unwrap(supportedSpecs))));
+ .bindSlice(uri, unwrap(supportedSpecs)));
}
}
diff --git a/slices/core/src/main/java/androidx/slice/SliceConvert.java b/slices/core/src/main/java/androidx/slice/SliceConvert.java
index 73ff368..fea5b4c 100644
--- a/slices/core/src/main/java/androidx/slice/SliceConvert.java
+++ b/slices/core/src/main/java/androidx/slice/SliceConvert.java
@@ -19,17 +19,16 @@
import static android.app.slice.SliceItem.FORMAT_ACTION;
import static android.app.slice.SliceItem.FORMAT_IMAGE;
import static android.app.slice.SliceItem.FORMAT_INT;
+import static android.app.slice.SliceItem.FORMAT_LONG;
import static android.app.slice.SliceItem.FORMAT_REMOTE_INPUT;
import static android.app.slice.SliceItem.FORMAT_SLICE;
import static android.app.slice.SliceItem.FORMAT_TEXT;
-import static android.app.slice.SliceItem.FORMAT_TIMESTAMP;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.collection.ArraySet;
import androidx.core.graphics.drawable.IconCompat;
-import java.util.List;
import java.util.Set;
/**
@@ -43,9 +42,8 @@
*/
public static android.app.slice.Slice unwrap(androidx.slice.Slice slice) {
android.app.slice.Slice.Builder builder = new android.app.slice.Slice.Builder(
- slice.getUri());
+ slice.getUri(), unwrap(slice.getSpec()));
builder.addHints(slice.getHints());
- builder.setSpec(unwrap(slice.getSpec()));
for (androidx.slice.SliceItem item : slice.getItems()) {
switch (item.getFormat()) {
case FORMAT_SLICE:
@@ -67,8 +65,8 @@
case FORMAT_INT:
builder.addInt(item.getInt(), item.getSubType(), item.getHints());
break;
- case FORMAT_TIMESTAMP:
- builder.addTimestamp(item.getTimestamp(), item.getSubType(), item.getHints());
+ case FORMAT_LONG:
+ builder.addLong(item.getLong(), item.getSubType(), item.getHints());
break;
}
}
@@ -119,8 +117,8 @@
case FORMAT_INT:
builder.addInt(item.getInt(), item.getSubType(), item.getHints());
break;
- case FORMAT_TIMESTAMP:
- builder.addTimestamp(item.getTimestamp(), item.getSubType(), item.getHints());
+ case FORMAT_LONG:
+ builder.addLong(item.getLong(), item.getSubType(), item.getHints());
break;
}
}
@@ -137,7 +135,7 @@
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public static Set<androidx.slice.SliceSpec> wrap(
- List<android.app.slice.SliceSpec> supportedSpecs) {
+ Set<android.app.slice.SliceSpec> supportedSpecs) {
Set<androidx.slice.SliceSpec> ret = new ArraySet<>();
for (android.app.slice.SliceSpec spec : supportedSpecs) {
ret.add(wrap(spec));
diff --git a/slices/core/src/main/java/androidx/slice/SliceItem.java b/slices/core/src/main/java/androidx/slice/SliceItem.java
index 964c5df..f6ae848 100644
--- a/slices/core/src/main/java/androidx/slice/SliceItem.java
+++ b/slices/core/src/main/java/androidx/slice/SliceItem.java
@@ -23,7 +23,6 @@
import static android.app.slice.SliceItem.FORMAT_REMOTE_INPUT;
import static android.app.slice.SliceItem.FORMAT_SLICE;
import static android.app.slice.SliceItem.FORMAT_TEXT;
-import static android.app.slice.SliceItem.FORMAT_TIMESTAMP;
import static androidx.slice.Slice.addHints;
@@ -59,7 +58,7 @@
* <li>{@link android.app.slice.SliceItem#FORMAT_IMAGE}</li>
* <li>{@link android.app.slice.SliceItem#FORMAT_ACTION}</li>
* <li>{@link android.app.slice.SliceItem#FORMAT_INT}</li>
- * <li>{@link android.app.slice.SliceItem#FORMAT_TIMESTAMP}</li>
+ * <li>{@link android.app.slice.SliceItem#FORMAT_LONG}</li>
* <p>
* The hints that a {@link SliceItem} are a set of strings which annotate
* the content. The hints that are guaranteed to be understood by the system
@@ -78,7 +77,7 @@
*/
@RestrictTo(Scope.LIBRARY)
@StringDef({FORMAT_SLICE, FORMAT_TEXT, FORMAT_IMAGE, FORMAT_ACTION, FORMAT_INT,
- FORMAT_TIMESTAMP, FORMAT_REMOTE_INPUT, FORMAT_LONG})
+ FORMAT_LONG, FORMAT_REMOTE_INPUT, FORMAT_LONG})
public @interface SliceType {
}
@@ -164,7 +163,7 @@
* <li>{@link android.app.slice.SliceItem#FORMAT_IMAGE}</li>
* <li>{@link android.app.slice.SliceItem#FORMAT_ACTION}</li>
* <li>{@link android.app.slice.SliceItem#FORMAT_INT}</li>
- * <li>{@link android.app.slice.SliceItem#FORMAT_TIMESTAMP}</li>
+ * <li>{@link android.app.slice.SliceItem#FORMAT_LONG}</li>
* <li>{@link android.app.slice.SliceItem#FORMAT_REMOTE_INPUT}</li>
* @see #getSubType() ()
*/
@@ -347,7 +346,7 @@
case FORMAT_INT:
dest.putInt(OBJ, (Integer) mObj);
break;
- case FORMAT_TIMESTAMP:
+ case FORMAT_LONG:
dest.putLong(OBJ, (Long) mObj);
break;
}
@@ -369,7 +368,7 @@
new Slice(in.getBundle(OBJ_2)));
case FORMAT_INT:
return in.getInt(OBJ);
- case FORMAT_TIMESTAMP:
+ case FORMAT_LONG:
return in.getLong(OBJ);
}
throw new RuntimeException("Unsupported type " + type);
@@ -391,8 +390,8 @@
return "Action";
case FORMAT_INT:
return "Int";
- case FORMAT_TIMESTAMP:
- return "Timestamp";
+ case FORMAT_LONG:
+ return "Long";
case FORMAT_REMOTE_INPUT:
return "RemoteInput";
}
diff --git a/slices/core/src/main/java/androidx/slice/compat/CompatPinnedList.java b/slices/core/src/main/java/androidx/slice/compat/CompatPinnedList.java
index 13f0c54..da602f5 100644
--- a/slices/core/src/main/java/androidx/slice/compat/CompatPinnedList.java
+++ b/slices/core/src/main/java/androidx/slice/compat/CompatPinnedList.java
@@ -28,6 +28,8 @@
import androidx.core.util.ObjectsCompat;
import androidx.slice.SliceSpec;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Set;
/**
@@ -70,6 +72,22 @@
return prefs;
}
+ /**
+ * Get pinned specs
+ */
+ public List<Uri> getPinnedSlices() {
+ List<Uri> pinned = new ArrayList<>();
+ for (String key : getPrefs().getAll().keySet()) {
+ if (key.startsWith(PIN_PREFIX)) {
+ Uri uri = Uri.parse(key.substring(PIN_PREFIX.length()));
+ if (!getPins(uri).isEmpty()) {
+ pinned.add(uri);
+ }
+ }
+ }
+ return pinned;
+ }
+
private Set<String> getPins(Uri uri) {
return getPrefs().getStringSet(PIN_PREFIX + uri.toString(), new ArraySet<String>());
}
diff --git a/slices/core/src/main/java/androidx/slice/compat/SlicePermissionActivity.java b/slices/core/src/main/java/androidx/slice/compat/SlicePermissionActivity.java
index 5feaa1e..5fa33ae 100644
--- a/slices/core/src/main/java/androidx/slice/compat/SlicePermissionActivity.java
+++ b/slices/core/src/main/java/androidx/slice/compat/SlicePermissionActivity.java
@@ -20,15 +20,20 @@
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.DialogInterface.OnDismissListener;
+import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.net.Uri;
import android.os.Bundle;
+import android.text.Html;
+import android.text.TextPaint;
+import android.text.TextUtils;
import android.util.Log;
import android.widget.TextView;
import androidx.annotation.RestrictTo;
import androidx.appcompat.app.AlertDialog;
+import androidx.core.text.BidiFormatter;
import androidx.slice.core.R;
/**
@@ -39,6 +44,8 @@
public class SlicePermissionActivity extends Activity implements OnClickListener,
OnDismissListener {
+ private static final float MAX_LABEL_SIZE_PX = 500f;
+
private static final String TAG = "SlicePermissionActivity";
private Uri mUri;
@@ -55,8 +62,12 @@
try {
PackageManager pm = getPackageManager();
- CharSequence app1 = pm.getApplicationInfo(mCallingPkg, 0).loadLabel(pm);
- CharSequence app2 = pm.getApplicationInfo(mProviderPkg, 0).loadLabel(pm);
+ CharSequence app1 = BidiFormatter.getInstance().unicodeWrap(
+ loadSafeLabel(pm, pm.getApplicationInfo(mCallingPkg, 0))
+ .toString());
+ CharSequence app2 = BidiFormatter.getInstance().unicodeWrap(
+ loadSafeLabel(pm, pm.getApplicationInfo(mProviderPkg, 0))
+ .toString());
AlertDialog dialog = new AlertDialog.Builder(this)
.setTitle(getString(R.string.abc_slice_permission_title, app1, app2))
.setView(R.layout.abc_slice_permission_request)
@@ -74,6 +85,45 @@
}
}
+ // Based on loadSafeLabel in PackageitemInfo
+ private CharSequence loadSafeLabel(PackageManager pm, ApplicationInfo appInfo) {
+ // loadLabel() always returns non-null
+ String label = appInfo.loadLabel(pm).toString();
+ // strip HTML tags to avoid <br> and other tags overwriting original message
+ String labelStr = Html.fromHtml(label).toString();
+
+ // If the label contains new line characters it may push the UI
+ // down to hide a part of it. Labels shouldn't have new line
+ // characters, so just truncate at the first time one is seen.
+ final int labelLength = labelStr.length();
+ int offset = 0;
+ while (offset < labelLength) {
+ final int codePoint = labelStr.codePointAt(offset);
+ final int type = Character.getType(codePoint);
+ if (type == Character.LINE_SEPARATOR
+ || type == Character.CONTROL
+ || type == Character.PARAGRAPH_SEPARATOR) {
+ labelStr = labelStr.substring(0, offset);
+ break;
+ }
+ // replace all non-break space to " " in order to be trimmed
+ if (type == Character.SPACE_SEPARATOR) {
+ labelStr = labelStr.substring(0, offset) + " " + labelStr.substring(offset
+ + Character.charCount(codePoint));
+ }
+ offset += Character.charCount(codePoint);
+ }
+
+ labelStr = labelStr.trim();
+ if (labelStr.isEmpty()) {
+ return appInfo.packageName;
+ }
+ TextPaint paint = new TextPaint();
+ paint.setTextSize(42);
+
+ return TextUtils.ellipsize(labelStr, paint, MAX_LABEL_SIZE_PX, TextUtils.TruncateAt.END);
+ }
+
@Override
public void onClick(DialogInterface dialog, int which) {
if (which == DialogInterface.BUTTON_POSITIVE) {
diff --git a/slices/core/src/main/java/androidx/slice/compat/SliceProviderCompat.java b/slices/core/src/main/java/androidx/slice/compat/SliceProviderCompat.java
index 2d06d8c..d62e9b5 100644
--- a/slices/core/src/main/java/androidx/slice/compat/SliceProviderCompat.java
+++ b/slices/core/src/main/java/androidx/slice/compat/SliceProviderCompat.java
@@ -25,6 +25,7 @@
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
+import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
@@ -62,6 +63,7 @@
private static final String TAG = "SliceProviderCompat";
private static final String DATA_PREFIX = "slice_data_";
private static final String PERMS_PREFIX = "slice_perms_";
+ private static final String ALL_FILES = DATA_PREFIX + "all_slice_files";
private static final long SLICE_BIND_ANR = 2000;
@@ -97,8 +99,18 @@
public SliceProviderCompat(SliceProvider provider) {
mProvider = provider;
- mPinnedList = new CompatPinnedList(provider.getContext(),
- DATA_PREFIX + getClass().getName());
+ String prefsFile = DATA_PREFIX + getClass().getName();
+ SharedPreferences allFiles = provider.getContext().getSharedPreferences(ALL_FILES, 0);
+ Set<String> files = allFiles.getStringSet(ALL_FILES, Collections.<String>emptySet());
+ if (!files.contains(prefsFile)) {
+ // Make sure this is editable.
+ files = new ArraySet<>(files);
+ files.add(prefsFile);
+ allFiles.edit()
+ .putStringSet(ALL_FILES, files)
+ .commit();
+ }
+ mPinnedList = new CompatPinnedList(provider.getContext(), prefsFile);
mPermissionManager = new CompatPermissionManager(provider.getContext(),
PERMS_PREFIX + getClass().getName());
}
@@ -117,12 +129,6 @@
public Bundle call(String method, String arg, Bundle extras) {
if (method.equals(METHOD_SLICE)) {
Uri uri = extras.getParcelable(EXTRA_BIND_URI);
- if (Binder.getCallingUid() != Process.myUid()) {
- getContext().enforceUriPermission(uri, Binder.getCallingPid(),
- Binder.getCallingUid(),
- Intent.FLAG_GRANT_WRITE_URI_PERMISSION,
- "Slice binding requires write access to the uri");
- }
Set<SliceSpec> specs = getSpecs(extras);
Slice s = handleBindSlice(uri, specs, getCallingPackage());
@@ -606,4 +612,17 @@
Log.e(TAG, "Unable to get slice descendants", e);
}
}
+
+ /**
+ * Compat version of {@link android.app.slice.SliceManager#getPinnedSlices}.
+ */
+ public static List<Uri> getPinnedSlices(Context context) {
+ ArrayList<Uri> pinnedSlices = new ArrayList<>();
+ SharedPreferences prefs = context.getSharedPreferences(ALL_FILES, 0);
+ Set<String> prefSet = prefs.getStringSet(ALL_FILES, Collections.<String>emptySet());
+ for (String pref : prefSet) {
+ pinnedSlices.addAll(new CompatPinnedList(context, pref).getPinnedSlices());
+ }
+ return pinnedSlices;
+ }
}
diff --git a/slices/core/src/main/java/androidx/slice/compat/SliceProviderWrapperContainer.java b/slices/core/src/main/java/androidx/slice/compat/SliceProviderWrapperContainer.java
index 8641530..c35793e 100644
--- a/slices/core/src/main/java/androidx/slice/compat/SliceProviderWrapperContainer.java
+++ b/slices/core/src/main/java/androidx/slice/compat/SliceProviderWrapperContainer.java
@@ -29,11 +29,10 @@
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
-import androidx.collection.ArraySet;
import androidx.slice.SliceConvert;
import java.util.Collection;
-import java.util.List;
+import java.util.Set;
/**
* @hide
@@ -64,8 +63,8 @@
}
@Override
- public Slice onBindSlice(Uri sliceUri, List<SliceSpec> supportedVersions) {
- androidx.slice.SliceProvider.setSpecs(new ArraySet<>(wrap(supportedVersions)));
+ public Slice onBindSlice(Uri sliceUri, Set<SliceSpec> supportedVersions) {
+ androidx.slice.SliceProvider.setSpecs(wrap(supportedVersions));
try {
return SliceConvert.unwrap(mSliceProvider.onBindSlice(sliceUri));
} finally {
diff --git a/slices/view/api/current.txt b/slices/view/api/current.txt
index 5a37826..aa95b81 100644
--- a/slices/view/api/current.txt
+++ b/slices/view/api/current.txt
@@ -5,6 +5,8 @@
method public abstract androidx.slice.Slice bindSlice(android.content.Intent);
method public abstract int checkSlicePermission(android.net.Uri, int, int);
method public static androidx.slice.SliceManager getInstance(android.content.Context);
+ method public abstract java.util.List<android.net.Uri> getPinnedSlices();
+ method public abstract java.util.Set<androidx.slice.SliceSpec> getPinnedSpecs(android.net.Uri);
method public abstract java.util.Collection<android.net.Uri> getSliceDescendants(android.net.Uri);
method public abstract void grantSlicePermission(java.lang.String, android.net.Uri);
method public abstract android.net.Uri mapIntentToUri(android.content.Intent);
diff --git a/slices/view/src/androidTest/java/androidx/slice/SliceManagerTest.java b/slices/view/src/androidTest/java/androidx/slice/SliceManagerTest.java
index b54335d..465db2b 100644
--- a/slices/view/src/androidTest/java/androidx/slice/SliceManagerTest.java
+++ b/slices/view/src/androidTest/java/androidx/slice/SliceManagerTest.java
@@ -17,6 +17,7 @@
package androidx.slice;
import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
@@ -44,6 +45,7 @@
import java.util.Arrays;
import java.util.Collection;
+import java.util.List;
import java.util.concurrent.Executor;
@RunWith(AndroidJUnit4.class)
@@ -88,6 +90,28 @@
}
@Test
+ public void testPinList() {
+ Uri uri = new Uri.Builder()
+ .scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(mContext.getPackageName())
+ .build();
+ Uri longerUri = uri.buildUpon().appendPath("something").build();
+ try {
+ mManager.pinSlice(uri);
+ mManager.pinSlice(longerUri);
+ verify(mSliceProvider, timeout(2000)).onSlicePinned(eq(longerUri));
+
+ List<Uri> uris = mManager.getPinnedSlices();
+ assertEquals(2, uris.size());
+ assertTrue(uris.contains(uri));
+ assertTrue(uris.contains(longerUri));
+ } finally {
+ mManager.unpinSlice(uri);
+ mManager.unpinSlice(longerUri);
+ }
+ }
+
+ @Test
public void testCallback() {
if (BuildCompat.isAtLeastP()) {
return;
diff --git a/slices/view/src/main/java/androidx/slice/SliceManager.java b/slices/view/src/main/java/androidx/slice/SliceManager.java
index 37a1960..83febcc 100644
--- a/slices/view/src/main/java/androidx/slice/SliceManager.java
+++ b/slices/view/src/main/java/androidx/slice/SliceManager.java
@@ -28,6 +28,7 @@
import androidx.core.os.BuildCompat;
import java.util.Collection;
+import java.util.List;
import java.util.Set;
import java.util.concurrent.Executor;
@@ -125,10 +126,8 @@
* <p>
* This is the set of specs supported for a specific pinned slice. It will take
* into account all clients and returns only specs supported by all.
- * @hide
* @see SliceSpec
*/
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public abstract @NonNull Set<SliceSpec> getPinnedSpecs(@NonNull Uri uri);
/**
@@ -233,6 +232,12 @@
public abstract @NonNull Collection<Uri> getSliceDescendants(@NonNull Uri uri);
/**
+ * Get the list of currently pinned slices for this app.
+ * @see SliceProvider#onSlicePinned
+ */
+ public abstract @NonNull List<Uri> getPinnedSlices();
+
+ /**
* Class that listens to changes in {@link Slice}s.
*/
public interface SliceCallback {
diff --git a/slices/view/src/main/java/androidx/slice/SliceManagerCompat.java b/slices/view/src/main/java/androidx/slice/SliceManagerCompat.java
index f8cda4d..0018c13 100644
--- a/slices/view/src/main/java/androidx/slice/SliceManagerCompat.java
+++ b/slices/view/src/main/java/androidx/slice/SliceManagerCompat.java
@@ -29,6 +29,7 @@
import androidx.slice.widget.SliceLiveData;
import java.util.Collection;
+import java.util.List;
import java.util.Set;
@@ -97,4 +98,9 @@
public Collection<Uri> getSliceDescendants(Uri uri) {
return SliceProviderCompat.getSliceDescendants(mContext, uri);
}
+
+ @Override
+ public List<Uri> getPinnedSlices() {
+ return SliceProviderCompat.getPinnedSlices(mContext);
+ }
}
diff --git a/slices/view/src/main/java/androidx/slice/SliceManagerWrapper.java b/slices/view/src/main/java/androidx/slice/SliceManagerWrapper.java
index 4c19b84..ec49fe2 100644
--- a/slices/view/src/main/java/androidx/slice/SliceManagerWrapper.java
+++ b/slices/view/src/main/java/androidx/slice/SliceManagerWrapper.java
@@ -30,7 +30,6 @@
import androidx.annotation.RestrictTo;
import androidx.core.content.PermissionChecker;
-import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
@@ -44,7 +43,7 @@
class SliceManagerWrapper extends SliceManagerBase {
private final android.app.slice.SliceManager mManager;
- private final List<SliceSpec> mSpecs;
+ private final Set<SliceSpec> mSpecs;
SliceManagerWrapper(Context context) {
this(context, context.getSystemService(android.app.slice.SliceManager.class));
@@ -53,7 +52,7 @@
SliceManagerWrapper(Context context, android.app.slice.SliceManager manager) {
super(context);
mManager = manager;
- mSpecs = new ArrayList<>(unwrap(SUPPORTED_SPECS));
+ mSpecs = unwrap(SUPPORTED_SPECS);
}
@Override
@@ -111,4 +110,9 @@
public Uri mapIntentToUri(@NonNull Intent intent) {
return mManager.mapIntentToUri(intent);
}
+
+ @Override
+ public List<Uri> getPinnedSlices() {
+ return mManager.getPinnedSlices();
+ }
}
diff --git a/slices/view/src/main/java/androidx/slice/SliceMetadata.java b/slices/view/src/main/java/androidx/slice/SliceMetadata.java
index 8a4ee03..0c4f614 100644
--- a/slices/view/src/main/java/androidx/slice/SliceMetadata.java
+++ b/slices/view/src/main/java/androidx/slice/SliceMetadata.java
@@ -23,9 +23,9 @@
import static android.app.slice.Slice.SUBTYPE_MAX;
import static android.app.slice.Slice.SUBTYPE_VALUE;
import static android.app.slice.SliceItem.FORMAT_INT;
+import static android.app.slice.SliceItem.FORMAT_LONG;
import static android.app.slice.SliceItem.FORMAT_SLICE;
import static android.app.slice.SliceItem.FORMAT_TEXT;
-import static android.app.slice.SliceItem.FORMAT_TIMESTAMP;
import static androidx.slice.core.SliceHints.HINT_KEYWORDS;
import static androidx.slice.core.SliceHints.HINT_LAST_UPDATED;
@@ -118,11 +118,11 @@
private SliceMetadata(@NonNull Context context, @NonNull Slice slice) {
mSlice = slice;
mContext = context;
- SliceItem ttlItem = SliceQuery.find(slice, FORMAT_TIMESTAMP, HINT_TTL, null);
+ SliceItem ttlItem = SliceQuery.find(slice, FORMAT_LONG, HINT_TTL, null);
if (ttlItem != null) {
mExpiry = ttlItem.getTimestamp();
}
- SliceItem updatedItem = SliceQuery.find(slice, FORMAT_TIMESTAMP, HINT_LAST_UPDATED, null);
+ SliceItem updatedItem = SliceQuery.find(slice, FORMAT_LONG, HINT_LAST_UPDATED, null);
if (updatedItem != null) {
mLastUpdated = updatedItem.getTimestamp();
}
diff --git a/slices/view/src/main/java/androidx/slice/widget/GridContent.java b/slices/view/src/main/java/androidx/slice/widget/GridContent.java
index 69d5c44..424143c 100644
--- a/slices/view/src/main/java/androidx/slice/widget/GridContent.java
+++ b/slices/view/src/main/java/androidx/slice/widget/GridContent.java
@@ -26,9 +26,9 @@
import static android.app.slice.SliceItem.FORMAT_ACTION;
import static android.app.slice.SliceItem.FORMAT_IMAGE;
import static android.app.slice.SliceItem.FORMAT_INT;
+import static android.app.slice.SliceItem.FORMAT_LONG;
import static android.app.slice.SliceItem.FORMAT_SLICE;
import static android.app.slice.SliceItem.FORMAT_TEXT;
-import static android.app.slice.SliceItem.FORMAT_TIMESTAMP;
import static androidx.slice.core.SliceHints.HINT_KEYWORDS;
import static androidx.slice.core.SliceHints.HINT_LAST_UPDATED;
@@ -295,7 +295,7 @@
if (SUBTYPE_CONTENT_DESCRIPTION.equals(item.getSubType())) {
mContentDescr = item;
} else if (mTextCount < 2 && (FORMAT_TEXT.equals(itemFormat)
- || FORMAT_TIMESTAMP.equals(itemFormat))) {
+ || FORMAT_LONG.equals(itemFormat))) {
mTextCount++;
mCellItems.add(item);
} else if (imageCount < 1 && FORMAT_IMAGE.equals(item.getFormat())) {
@@ -340,7 +340,7 @@
|| cellItem.hasAnyHints(HINT_KEYWORDS, HINT_TTL, HINT_LAST_UPDATED);
return !isNonCellContent
&& (FORMAT_TEXT.equals(format)
- || FORMAT_TIMESTAMP.equals(format)
+ || FORMAT_LONG.equals(format)
|| FORMAT_IMAGE.equals(format));
}
diff --git a/slices/view/src/main/java/androidx/slice/widget/GridRowView.java b/slices/view/src/main/java/androidx/slice/widget/GridRowView.java
index 4b8078c..7423b02 100644
--- a/slices/view/src/main/java/androidx/slice/widget/GridRowView.java
+++ b/slices/view/src/main/java/androidx/slice/widget/GridRowView.java
@@ -21,9 +21,9 @@
import static android.app.slice.Slice.HINT_TITLE;
import static android.app.slice.SliceItem.FORMAT_ACTION;
import static android.app.slice.SliceItem.FORMAT_IMAGE;
+import static android.app.slice.SliceItem.FORMAT_LONG;
import static android.app.slice.SliceItem.FORMAT_SLICE;
import static android.app.slice.SliceItem.FORMAT_TEXT;
-import static android.app.slice.SliceItem.FORMAT_TIMESTAMP;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
@@ -258,7 +258,7 @@
SliceItem item = cellItems.get(i);
final String itemFormat = item.getFormat();
if (textCount < maxCellText && (FORMAT_TEXT.equals(itemFormat)
- || FORMAT_TIMESTAMP.equals(itemFormat))) {
+ || FORMAT_LONG.equals(itemFormat))) {
if (textItems != null && !textItems.contains(item)) {
continue;
}
@@ -305,13 +305,13 @@
private boolean addItem(SliceItem item, int color, ViewGroup container, boolean singleItem) {
final String format = item.getFormat();
View addedView = null;
- if (FORMAT_TEXT.equals(format) || FORMAT_TIMESTAMP.equals(format)) {
+ if (FORMAT_TEXT.equals(format) || FORMAT_LONG.equals(format)) {
boolean title = SliceQuery.hasAnyHints(item, HINT_LARGE, HINT_TITLE);
TextView tv = (TextView) LayoutInflater.from(getContext()).inflate(title
? TITLE_TEXT_LAYOUT : TEXT_LAYOUT, null);
tv.setTextSize(TypedValue.COMPLEX_UNIT_PX, title ? mTitleSize : mSubtitleSize);
tv.setTextColor(title ? mTitleColor : mSubtitleColor);
- CharSequence text = FORMAT_TIMESTAMP.equals(format)
+ CharSequence text = FORMAT_LONG.equals(format)
? SliceViewUtil.getRelativeTimeString(item.getTimestamp())
: item.getText();
tv.setText(text);
diff --git a/slices/view/src/main/java/androidx/slice/widget/LargeSliceAdapter.java b/slices/view/src/main/java/androidx/slice/widget/LargeSliceAdapter.java
index e09405c..9578e21 100644
--- a/slices/view/src/main/java/androidx/slice/widget/LargeSliceAdapter.java
+++ b/slices/view/src/main/java/androidx/slice/widget/LargeSliceAdapter.java
@@ -191,8 +191,6 @@
null);
break;
}
- int mode = mParent != null ? mParent.getMode() : MODE_LARGE;
- ((SliceChildView) v).setMode(mode);
return v;
}
@@ -248,6 +246,8 @@
mSliceChildView.setOnTouchListener(this);
final boolean isHeader = position == HEADER_INDEX;
+ int mode = mParent != null ? mParent.getMode() : MODE_LARGE;
+ mSliceChildView.setMode(mode);
mSliceChildView.setTint(mColor);
mSliceChildView.setStyle(mAttrs, mDefStyleAttr, mDefStyleRes);
mSliceChildView.setSliceItem(item, isHeader, position, mSliceObserver);
diff --git a/slices/view/src/main/java/androidx/slice/widget/RowContent.java b/slices/view/src/main/java/androidx/slice/widget/RowContent.java
index bc3826f..0c8d4a6 100644
--- a/slices/view/src/main/java/androidx/slice/widget/RowContent.java
+++ b/slices/view/src/main/java/androidx/slice/widget/RowContent.java
@@ -27,10 +27,10 @@
import static android.app.slice.SliceItem.FORMAT_ACTION;
import static android.app.slice.SliceItem.FORMAT_IMAGE;
import static android.app.slice.SliceItem.FORMAT_INT;
+import static android.app.slice.SliceItem.FORMAT_LONG;
import static android.app.slice.SliceItem.FORMAT_REMOTE_INPUT;
import static android.app.slice.SliceItem.FORMAT_SLICE;
import static android.app.slice.SliceItem.FORMAT_TEXT;
-import static android.app.slice.SliceItem.FORMAT_TIMESTAMP;
import static androidx.slice.core.SliceHints.HINT_KEYWORDS;
import static androidx.slice.core.SliceHints.HINT_LAST_UPDATED;
@@ -157,11 +157,11 @@
}
// Special rules for end items: only one timestamp
boolean hasTimestamp = mStartItem != null
- && FORMAT_TIMESTAMP.equals(mStartItem.getFormat());
+ && FORMAT_LONG.equals(mStartItem.getFormat());
for (int i = 0; i < endItems.size(); i++) {
final SliceItem item = endItems.get(i);
boolean isAction = SliceQuery.find(item, FORMAT_ACTION) != null;
- if (FORMAT_TIMESTAMP.equals(item.getFormat())) {
+ if (FORMAT_LONG.equals(item.getFormat())) {
if (!hasTimestamp) {
hasTimestamp = true;
mEndItems.add(item);
@@ -382,7 +382,7 @@
return (FORMAT_TEXT.equals(itemFormat)
&& !SUBTYPE_CONTENT_DESCRIPTION.equals(item.getSubType()))
|| FORMAT_IMAGE.equals(itemFormat)
- || FORMAT_TIMESTAMP.equals(itemFormat)
+ || FORMAT_LONG.equals(itemFormat)
|| FORMAT_REMOTE_INPUT.equals(itemFormat)
|| (FORMAT_SLICE.equals(itemFormat) && item.hasHint(HINT_TITLE)
&& !item.hasHint(HINT_SHORTCUT))
@@ -400,7 +400,7 @@
final String type = item.getFormat();
return (FORMAT_ACTION.equals(type) && (SliceQuery.find(item, FORMAT_IMAGE) != null))
|| FORMAT_IMAGE.equals(type)
- || (FORMAT_TIMESTAMP.equals(type)
+ || (FORMAT_LONG.equals(type)
&& !item.hasAnyHints(HINT_TTL, HINT_LAST_UPDATED));
}
}
diff --git a/slices/view/src/main/java/androidx/slice/widget/RowView.java b/slices/view/src/main/java/androidx/slice/widget/RowView.java
index a7101fe..2840db9 100644
--- a/slices/view/src/main/java/androidx/slice/widget/RowView.java
+++ b/slices/view/src/main/java/androidx/slice/widget/RowView.java
@@ -26,8 +26,8 @@
import static android.app.slice.SliceItem.FORMAT_ACTION;
import static android.app.slice.SliceItem.FORMAT_IMAGE;
import static android.app.slice.SliceItem.FORMAT_INT;
+import static android.app.slice.SliceItem.FORMAT_LONG;
import static android.app.slice.SliceItem.FORMAT_SLICE;
-import static android.app.slice.SliceItem.FORMAT_TIMESTAMP;
import static androidx.slice.core.SliceHints.ICON_IMAGE;
import static androidx.slice.core.SliceHints.SMALL_IMAGE;
@@ -278,7 +278,7 @@
// If we're here we can can show end items; check for top level actions first
List<SliceItem> endItems = mRowContent.getEndItems();
- if (mIsHeader && mHeaderActions != null && mHeaderActions.size() > 0) {
+ if (mHeaderActions != null && mHeaderActions.size() > 0) {
// Use these if we have them instead
endItems = mHeaderActions;
}
@@ -460,7 +460,7 @@
if (FORMAT_IMAGE.equals(sliceItem.getFormat())) {
icon = sliceItem.getIcon();
imageMode = sliceItem.hasHint(HINT_NO_TINT) ? SMALL_IMAGE : ICON_IMAGE;
- } else if (FORMAT_TIMESTAMP.equals(sliceItem.getFormat())) {
+ } else if (FORMAT_LONG.equals(sliceItem.getFormat())) {
timeStamp = sliceItem;
}
View addedView = null;
diff --git a/slices/view/src/main/java/androidx/slice/widget/ShortcutView.java b/slices/view/src/main/java/androidx/slice/widget/ShortcutView.java
index 37ef17b..3afda5d 100644
--- a/slices/view/src/main/java/androidx/slice/widget/ShortcutView.java
+++ b/slices/view/src/main/java/androidx/slice/widget/ShortcutView.java
@@ -195,7 +195,7 @@
if (mActionItem == null) {
mActionItem = new SliceItem(PendingIntent.getActivity(context, 0,
pm.getLaunchIntentForPackage(appInfo.packageName), 0),
- new Slice.Builder(slice.getUri()).build(), FORMAT_SLICE,
+ new Slice.Builder(slice.getUri()).build(), FORMAT_ACTION,
null /* subtype */, null);
}
}
diff --git a/webkit/api/current.txt b/webkit/api/current.txt
index 7084a21..60dbb9c 100644
--- a/webkit/api/current.txt
+++ b/webkit/api/current.txt
@@ -33,6 +33,10 @@
method public abstract int getErrorCode();
}
+ public class WebResourceRequestCompat {
+ method public static boolean isRedirect(android.webkit.WebResourceRequest);
+ }
+
public class WebSettingsCompat {
method public static int getDisabledActionModeMenuItems(android.webkit.WebSettings);
method public static boolean getOffscreenPreRaster(android.webkit.WebSettings);
@@ -81,6 +85,7 @@
field public static final java.lang.String SHOULD_OVERRIDE_WITH_REDIRECTS = "SHOULD_OVERRIDE_WITH_REDIRECTS";
field public static final java.lang.String START_SAFE_BROWSING = "START_SAFE_BROWSING";
field public static final java.lang.String VISUAL_STATE_CALLBACK = "VISUAL_STATE_CALLBACK";
+ field public static final java.lang.String WEB_RESOURCE_REQUEST_IS_REDIRECT = "WEB_RESOURCE_REQUEST_IS_REDIRECT";
}
}
diff --git a/webkit/src/main/java/androidx/webkit/WebResourceRequestCompat.java b/webkit/src/main/java/androidx/webkit/WebResourceRequestCompat.java
new file mode 100644
index 0000000..783efca
--- /dev/null
+++ b/webkit/src/main/java/androidx/webkit/WebResourceRequestCompat.java
@@ -0,0 +1,60 @@
+/*
+ * 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 androidx.webkit;
+
+import android.annotation.SuppressLint;
+import android.webkit.WebResourceRequest;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresFeature;
+import androidx.webkit.internal.WebResourceRequestAdapter;
+import androidx.webkit.internal.WebViewFeatureInternal;
+import androidx.webkit.internal.WebViewGlueCommunicator;
+
+// TODO(gsennton) add a test for this class
+
+/**
+ * Compatibility version of {@link WebResourceRequest}.
+ */
+public class WebResourceRequestCompat {
+
+ // Developers should not be able to instantiate this class.
+ private WebResourceRequestCompat() {}
+
+ /**
+ * Gets whether the request was a result of a server-side redirect.
+ *
+ * @return whether the request was a result of a server-side redirect.
+ */
+ @SuppressLint("NewApi")
+ @RequiresFeature(name = WebViewFeature.WEB_RESOURCE_REQUEST_IS_REDIRECT,
+ enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
+ public static boolean isRedirect(@NonNull WebResourceRequest request) {
+ WebViewFeatureInternal feature = WebViewFeatureInternal.WEB_RESOURCE_REQUEST_IS_REDIRECT;
+ if (feature.isSupportedByFramework()) {
+ return request.isRedirect();
+ } else if (feature.isSupportedByWebView()) {
+ return getAdapter(request).isRedirect();
+ } else {
+ throw WebViewFeatureInternal.getUnsupportedOperationException();
+ }
+ }
+
+ private static WebResourceRequestAdapter getAdapter(WebResourceRequest request) {
+ return WebViewGlueCommunicator.getCompatConverter().convertWebResourceRequest(request);
+ }
+}
diff --git a/webkit/src/main/java/androidx/webkit/WebViewCompat.java b/webkit/src/main/java/androidx/webkit/WebViewCompat.java
index 6e82a69..bb1c4fc 100644
--- a/webkit/src/main/java/androidx/webkit/WebViewCompat.java
+++ b/webkit/src/main/java/androidx/webkit/WebViewCompat.java
@@ -344,7 +344,7 @@
throw new RuntimeException("A WebView method was called on thread '"
+ Thread.currentThread().getName() + "'. "
+ "All WebView methods must be called on the same thread. "
- + "(Expected Looper " + webview.getLooper() + " called on "
+ + "(Expected Looper " + webview.getWebViewLooper() + " called on "
+ Looper.myLooper() + ", FYI main Looper is " + Looper.getMainLooper()
+ ")");
}
diff --git a/webkit/src/main/java/androidx/webkit/WebViewFeature.java b/webkit/src/main/java/androidx/webkit/WebViewFeature.java
index b2715df..5ee4c2c 100644
--- a/webkit/src/main/java/androidx/webkit/WebViewFeature.java
+++ b/webkit/src/main/java/androidx/webkit/WebViewFeature.java
@@ -62,7 +62,8 @@
RECEIVE_WEB_RESOURCE_ERROR,
RECEIVE_HTTP_ERROR,
SHOULD_OVERRIDE_WITH_REDIRECTS,
- SAFE_BROWSING_HIT
+ SAFE_BROWSING_HIT,
+ WEB_RESOURCE_REQUEST_IS_REDIRECT
})
@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.PARAMETER, ElementType.METHOD})
@@ -208,6 +209,14 @@
public static final String SAFE_BROWSING_HIT = Features.SAFE_BROWSING_HIT;
/**
+ * Feature for {@link #isFeatureSupported(String)}.
+ * This feature covers
+ * {@link WebResourceRequestCompat#isRedirect(WebResourceRequest)}.
+ */
+ public static final String WEB_RESOURCE_REQUEST_IS_REDIRECT =
+ Features.WEB_RESOURCE_REQUEST_IS_REDIRECT;
+
+ /**
* Return whether a feature is supported at run-time. This depends on the Android version of the
* device and the WebView APK on the device.
*/
diff --git a/webkit/src/main/java/androidx/webkit/internal/WebResourceRequestAdapter.java b/webkit/src/main/java/androidx/webkit/internal/WebResourceRequestAdapter.java
new file mode 100644
index 0000000..0d6a05d
--- /dev/null
+++ b/webkit/src/main/java/androidx/webkit/internal/WebResourceRequestAdapter.java
@@ -0,0 +1,41 @@
+/*
+ * 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 androidx.webkit.internal;
+
+import android.webkit.WebResourceRequest;
+
+import org.chromium.support_lib_boundary.WebResourceRequestBoundaryInterface;
+
+/**
+ * Adapter between {@link androidx.webkit.WebResourceRequestCompat} and
+ * {@link org.chromium.support_lib_boundary.WebResourceRequestBoundaryInterface}.
+ */
+public class WebResourceRequestAdapter {
+ private final WebResourceRequestBoundaryInterface mBoundaryInterface;
+
+ public WebResourceRequestAdapter(WebResourceRequestBoundaryInterface boundaryInterface) {
+ mBoundaryInterface = boundaryInterface;
+ }
+
+ /**
+ * Adapter method for
+ * {@link androidx.webkit.WebResourceRequestCompat#isRedirect(WebResourceRequest)}.
+ */
+ public boolean isRedirect() {
+ return mBoundaryInterface.isRedirect();
+ }
+}
diff --git a/webkit/src/main/java/androidx/webkit/internal/WebViewFeatureInternal.java b/webkit/src/main/java/androidx/webkit/internal/WebViewFeatureInternal.java
index d2ff4a4..07c6b5f 100644
--- a/webkit/src/main/java/androidx/webkit/internal/WebViewFeatureInternal.java
+++ b/webkit/src/main/java/androidx/webkit/internal/WebViewFeatureInternal.java
@@ -23,6 +23,7 @@
import android.webkit.WebSettings;
import androidx.webkit.ServiceWorkerClientCompat;
+import androidx.webkit.WebResourceRequestCompat;
import androidx.webkit.WebViewClientCompat;
import androidx.webkit.WebViewCompat;
import androidx.webkit.WebViewFeature;
@@ -154,7 +155,14 @@
* {@link WebViewClientCompat#onSafeBrowsingHit(android.webkit.WebView,
* WebResourceRequest, int, SafeBrowsingResponseCompat)}.
*/
- SAFE_BROWSING_HIT(WebViewFeature.SAFE_BROWSING_HIT, Build.VERSION_CODES.O_MR1);
+ SAFE_BROWSING_HIT(WebViewFeature.SAFE_BROWSING_HIT, Build.VERSION_CODES.O_MR1),
+
+ /**
+ * This feature covers
+ * {@link WebResourceRequestCompat#isRedirect(WebResourceRequest)}.
+ */
+ WEB_RESOURCE_REQUEST_IS_REDIRECT(WebViewFeature.WEB_RESOURCE_REQUEST_IS_REDIRECT,
+ Build.VERSION_CODES.N);
private final String mFeatureValue;
private final int mOsVersion;
diff --git a/webkit/src/main/java/androidx/webkit/internal/WebkitToCompatConverter.java b/webkit/src/main/java/androidx/webkit/internal/WebkitToCompatConverter.java
index 4d264a3..e333480 100644
--- a/webkit/src/main/java/androidx/webkit/internal/WebkitToCompatConverter.java
+++ b/webkit/src/main/java/androidx/webkit/internal/WebkitToCompatConverter.java
@@ -17,9 +17,11 @@
package androidx.webkit.internal;
import android.webkit.ServiceWorkerWebSettings;
+import android.webkit.WebResourceRequest;
import android.webkit.WebSettings;
import org.chromium.support_lib_boundary.ServiceWorkerWebSettingsBoundaryInterface;
+import org.chromium.support_lib_boundary.WebResourceRequestBoundaryInterface;
import org.chromium.support_lib_boundary.WebSettingsBoundaryInterface;
import org.chromium.support_lib_boundary.WebkitToCompatConverterBoundaryInterface;
import org.chromium.support_lib_boundary.util.BoundaryInterfaceReflectionUtil;
@@ -56,4 +58,14 @@
ServiceWorkerWebSettingsBoundaryInterface.class,
mImpl.convertServiceWorkerSettings(settings));
}
+
+ /**
+ * Return a {@link WebResourceRequestAdapter} linked to the given {@link WebResourceRequest} so
+ * that calls on either of those objects affect the other object.
+ */
+ public WebResourceRequestAdapter convertWebResourceRequest(WebResourceRequest request) {
+ return new WebResourceRequestAdapter(BoundaryInterfaceReflectionUtil.castToSuppLibClass(
+ WebResourceRequestBoundaryInterface.class,
+ mImpl.convertWebResourceRequest(request)));
+ }
}