Merge "[WebView Support Library] Add WebViewClientCompat feature flags" into pi-androidx-dev
diff --git a/car/res/drawable/car_borderless_button_background.xml b/car/res/drawable/car_borderless_button_background.xml
index 0ef4bea..a8add07 100644
--- a/car/res/drawable/car_borderless_button_background.xml
+++ b/car/res/drawable/car_borderless_button_background.xml
@@ -18,6 +18,6 @@
xmlns:android="http://schemas.android.com/apk/res/android"
android:color="?attr/listItemBackgroundColor">
<item android:id="@android:id/mask">
- <color android:color="#42ffffff "/>
+ <color android:color="#42ffffff"/>
</item>
</ripple>
diff --git a/car/res/layout/car_alert_dialog.xml b/car/res/layout/car_alert_dialog.xml
index 46da691..704c8ad 100644
--- a/car/res/layout/car_alert_dialog.xml
+++ b/car/res/layout/car_alert_dialog.xml
@@ -38,7 +38,7 @@
<TextView
android:id="@+id/title"
- android:layout_width="wrap_content"
+ android:layout_width="match_parent"
android:layout_height="@dimen/car_dialog_header_height"
android:gravity="center_vertical|start"
android:visibility="gone"
diff --git a/car/res/layout/car_list_item_seekbar_content.xml b/car/res/layout/car_list_item_seekbar_content.xml
index eedbe73..6e3e33a 100644
--- a/car/res/layout/car_list_item_seekbar_content.xml
+++ b/car/res/layout/car_list_item_seekbar_content.xml
@@ -33,6 +33,8 @@
android:id="@+id/seek_bar_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/car_padding_1"
+ android:layout_marginBottom="@dimen/car_padding_1"
android:orientation="vertical">
<TextView
android:id="@+id/text"
diff --git a/car/res/layout/car_list_item_text_content.xml b/car/res/layout/car_list_item_text_content.xml
index 0e5d4b9..b5f1a03 100644
--- a/car/res/layout/car_list_item_text_content.xml
+++ b/car/res/layout/car_list_item_text_content.xml
@@ -59,11 +59,11 @@
<View
android:id="@+id/switch_divider"
android:layout_centerVertical="true"
- android:layout_toStartOf="@id/switch_widget"
+ android:layout_toStartOf="@+id/switch_widget"
android:layout_marginEnd="@dimen/car_padding_4"
style="@style/CarListVerticalDivider"/>
<Switch
- android:id="@+id/switch_widget"
+ android:id="@id/switch_widget"
android:layout_centerVertical="true"
android:layout_width="@dimen/car_primary_icon_size"
android:layout_height="@dimen/car_primary_icon_size"
@@ -75,28 +75,28 @@
<View
android:id="@+id/action2_divider"
android:layout_centerVertical="true"
- android:layout_toStartOf="@id/action2"
+ android:layout_toStartOf="@+id/action2"
android:layout_marginEnd="@dimen/car_padding_4"
style="@style/CarListVerticalDivider"/>
<Button
- android:id="@+id/action2"
+ android:id="@id/action2"
android:layout_centerVertical="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_toStartOf="@id/action1_divider"
+ android:layout_toStartOf="@+id/action1_divider"
android:layout_marginEnd="@dimen/car_padding_4"
android:ellipsize="end"
android:maxLength="@integer/car_borderless_button_text_length_limit"
android:maxLines="1"
style="?android:attr/borderlessButtonStyle"/>
<View
- android:id="@+id/action1_divider"
+ android:id="@id/action1_divider"
android:layout_centerVertical="true"
- android:layout_toStartOf="@id/action1"
+ android:layout_toStartOf="@+id/action1"
android:layout_marginEnd="@dimen/car_padding_4"
style="@style/CarListVerticalDivider"/>
<Button
- android:id="@+id/action1"
+ android:id="@id/action1"
android:layout_centerVertical="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
diff --git a/car/res/values/dimens.xml b/car/res/values/dimens.xml
index c310ec7..51810ad 100644
--- a/car/res/values/dimens.xml
+++ b/car/res/values/dimens.xml
@@ -144,7 +144,7 @@
<!-- Seekbar -->
<dimen name="car_seekbar_height">6dp</dimen>
- <dimen name="car_seekbar_thumb_size">20dp</dimen>
+ <dimen name="car_seekbar_thumb_size">24dp</dimen>
<dimen name="car_seekbar_thumb_stroke">1dp</dimen>
<!-- Scroll Bar Thumb -->
diff --git a/car/res/values/styles.xml b/car/res/values/styles.xml
index 7e32c39..4b547ad 100644
--- a/car/res/values/styles.xml
+++ b/car/res/values/styles.xml
@@ -328,7 +328,6 @@
<item name="android:maxLines">1</item>
<item name="android:textAppearance">@style/TextAppearance.Car.Title2</item>
<item name="android:ellipsize">end</item>
- <item name="android:textAlignment">viewStart</item>
</style>
<!-- Title text for a dialog that is fixed in a light color. -->
diff --git a/car/src/main/java/androidx/car/app/CarAlertDialog.java b/car/src/main/java/androidx/car/app/CarAlertDialog.java
index 453ad4e..58262b6 100644
--- a/car/src/main/java/androidx/car/app/CarAlertDialog.java
+++ b/car/src/main/java/androidx/car/app/CarAlertDialog.java
@@ -23,6 +23,7 @@
import android.os.Bundle;
import android.text.TextUtils;
import android.util.TypedValue;
+import android.view.Gravity;
import android.view.MotionEvent;
import android.view.TouchDelegate;
import android.view.View;
@@ -90,17 +91,24 @@
private void setTitleInternal(CharSequence title) {
boolean hasTitle = !TextUtils.isEmpty(title);
+ boolean hasBody = mBodyView.getVisibility() == View.VISIBLE;
+ boolean hasButton = mButtonPanel.getVisibility() == View.VISIBLE;
mTitleView.setText(title);
mTitleView.setVisibility(hasTitle ? View.VISIBLE : View.GONE);
+ // Center title if there is no button.
+ mTitleView.setGravity(hasButton ? Gravity.CENTER_VERTICAL | Gravity.START : Gravity.CENTER);
+
// If there's a title, then remove the padding at the top of the content view.
int topPadding = hasTitle ? 0 : mTopPadding;
+ // If there is only title, also remove the padding at the bottom so title is centered.
+ int bottomPadding = !hasButton && !hasBody ? 0 : mContentView.getPaddingBottom();
mContentView.setPaddingRelative(
mContentView.getPaddingStart(),
topPadding,
mContentView.getPaddingEnd(),
- mContentView.getPaddingBottom());
+ bottomPadding);
}
private void setBody(CharSequence body) {
@@ -226,10 +234,12 @@
* contents based on what data is present.
*/
private void initializeDialogWithData() {
- setTitleInternal(mData.mTitle);
setBody(mData.mBody);
setPositiveButton(mData.mPositiveButtonText);
setNegativeButton(mData.mNegativeButtonText);
+ // setTitleInternal() should be called last because we want to center title and adjust
+ // padding depending on body/button configuration.
+ setTitleInternal(mData.mTitle);
}
/**
diff --git a/compat/src/main/java/androidx/core/graphics/drawable/IconCompat.java b/compat/src/main/java/androidx/core/graphics/drawable/IconCompat.java
index 366ddf3..4c296ea 100644
--- a/compat/src/main/java/androidx/core/graphics/drawable/IconCompat.java
+++ b/compat/src/main/java/androidx/core/graphics/drawable/IconCompat.java
@@ -569,6 +569,58 @@
return bundle;
}
+ @Override
+ public String toString() {
+ if (mType == TYPE_UNKOWN) {
+ return String.valueOf(mObj1);
+ }
+ final StringBuilder sb = new StringBuilder("Icon(typ=").append(typeToString(mType));
+ switch (mType) {
+ case TYPE_BITMAP:
+ case TYPE_ADAPTIVE_BITMAP:
+ sb.append(" size=")
+ .append(((Bitmap) mObj1).getWidth())
+ .append("x")
+ .append(((Bitmap) mObj1).getHeight());
+ break;
+ case TYPE_RESOURCE:
+ sb.append(" pkg=")
+ .append(getResPackage())
+ .append(" id=")
+ .append(String.format("0x%08x", getResId()));
+ break;
+ case TYPE_DATA:
+ sb.append(" len=").append(mInt1);
+ if (mInt2 != 0) {
+ sb.append(" off=").append(mInt2);
+ }
+ break;
+ case TYPE_URI:
+ sb.append(" uri=").append(mObj1);
+ break;
+ }
+ if (mTintList != null) {
+ sb.append(" tint=");
+ sb.append(mTintList);
+ }
+ if (mTintMode != DEFAULT_TINT_MODE) {
+ sb.append(" mode=").append(mTintMode);
+ }
+ sb.append(")");
+ return sb.toString();
+ }
+
+ private static String typeToString(int x) {
+ switch (x) {
+ case TYPE_BITMAP: return "BITMAP";
+ case TYPE_ADAPTIVE_BITMAP: return "BITMAP_MASKABLE";
+ case TYPE_DATA: return "DATA";
+ case TYPE_RESOURCE: return "RESOURCE";
+ case TYPE_URI: return "URI";
+ default: return "UNKNOWN";
+ }
+ }
+
/**
* Extracts an icon from a bundle that was added using {@link #toBundle()}.
*/
diff --git a/jetifier/jetifier/core/src/main/resources/default.generated.config b/jetifier/jetifier/core/src/main/resources/default.generated.config
index 66dfa5a..3acc5fc 100644
--- a/jetifier/jetifier/core/src/main/resources/default.generated.config
+++ b/jetifier/jetifier/core/src/main/resources/default.generated.config
@@ -2124,7 +2124,7 @@
"to": [
{
"groupId": "androidx.slice",
- "artifactId": "slices-core",
+ "artifactId": "slice-core",
"version": "1.0.0"
}
]
@@ -2138,7 +2138,7 @@
"to": [
{
"groupId": "androidx.slice",
- "artifactId": "slices-builders",
+ "artifactId": "slice-builders",
"version": "1.0.0"
}
]
@@ -2152,7 +2152,7 @@
"to": [
{
"groupId": "androidx.slice",
- "artifactId": "slices-view",
+ "artifactId": "slice-view",
"version": "1.0.0"
}
]
diff --git a/media/api/current.txt b/media/api/current.txt
index d7df744..a9e2161 100644
--- a/media/api/current.txt
+++ b/media/api/current.txt
@@ -565,6 +565,41 @@
method public androidx.media.AudioAttributesCompat.Builder setUsage(int);
}
+ public final class DataSourceDesc {
+ method public long getEndPosition();
+ method public java.io.FileDescriptor getFileDescriptor();
+ method public long getFileDescriptorLength();
+ method public long getFileDescriptorOffset();
+ method public androidx.media.Media2DataSource getMedia2DataSource();
+ method public java.lang.String getMediaId();
+ method public long getStartPosition();
+ method public int getType();
+ method public android.net.Uri getUri();
+ method public android.content.Context getUriContext();
+ method public java.util.List<java.net.HttpCookie> getUriCookies();
+ method public java.util.Map<java.lang.String, java.lang.String> getUriHeaders();
+ field public static final long FD_LENGTH_UNKNOWN = 576460752303423487L; // 0x7ffffffffffffffL
+ field public static final long POSITION_UNKNOWN = 576460752303423487L; // 0x7ffffffffffffffL
+ field public static final int TYPE_CALLBACK = 1; // 0x1
+ field public static final int TYPE_FD = 2; // 0x2
+ field public static final int TYPE_NONE = 0; // 0x0
+ field public static final int TYPE_URI = 3; // 0x3
+ }
+
+ public static class DataSourceDesc.Builder {
+ ctor public DataSourceDesc.Builder();
+ ctor public DataSourceDesc.Builder(androidx.media.DataSourceDesc);
+ method public androidx.media.DataSourceDesc build();
+ method public androidx.media.DataSourceDesc.Builder setDataSource(androidx.media.Media2DataSource);
+ method public androidx.media.DataSourceDesc.Builder setDataSource(java.io.FileDescriptor);
+ method public androidx.media.DataSourceDesc.Builder setDataSource(java.io.FileDescriptor, long, long);
+ method public androidx.media.DataSourceDesc.Builder setDataSource(android.content.Context, android.net.Uri);
+ method public androidx.media.DataSourceDesc.Builder setDataSource(android.content.Context, android.net.Uri, java.util.Map<java.lang.String, java.lang.String>, java.util.List<java.net.HttpCookie>);
+ method public androidx.media.DataSourceDesc.Builder setEndPosition(long);
+ method public androidx.media.DataSourceDesc.Builder setMediaId(java.lang.String);
+ method public androidx.media.DataSourceDesc.Builder setStartPosition(long);
+ }
+
public abstract class MediaBrowserServiceCompat extends android.app.Service {
ctor public MediaBrowserServiceCompat();
method public void dump(java.io.FileDescriptor, java.io.PrintWriter, java.lang.String[]);
@@ -600,6 +635,487 @@
method public void sendResult(T);
}
+ public class MediaController2 implements java.lang.AutoCloseable {
+ ctor public MediaController2(android.content.Context, androidx.media.SessionToken2, java.util.concurrent.Executor, androidx.media.MediaController2.ControllerCallback);
+ method public void addPlaylistItem(int, androidx.media.MediaItem2);
+ method public void adjustVolume(int, int);
+ method public void close();
+ method public void fastForward();
+ method public long getBufferedPosition();
+ method public int getBufferingState();
+ method public androidx.media.MediaItem2 getCurrentMediaItem();
+ method public long getCurrentPosition();
+ method public long getDuration();
+ method public androidx.media.MediaController2.PlaybackInfo getPlaybackInfo();
+ method public float getPlaybackSpeed();
+ method public int getPlayerState();
+ method public java.util.List<androidx.media.MediaItem2> getPlaylist();
+ method public androidx.media.MediaMetadata2 getPlaylistMetadata();
+ method public int getRepeatMode();
+ method public android.app.PendingIntent getSessionActivity();
+ method public androidx.media.SessionToken2 getSessionToken();
+ method public int getShuffleMode();
+ method public boolean isConnected();
+ method public void pause();
+ method public void play();
+ method public void playFromMediaId(java.lang.String, android.os.Bundle);
+ method public void playFromSearch(java.lang.String, android.os.Bundle);
+ method public void playFromUri(android.net.Uri, android.os.Bundle);
+ method public void prepare();
+ method public void prepareFromMediaId(java.lang.String, android.os.Bundle);
+ method public void prepareFromSearch(java.lang.String, android.os.Bundle);
+ method public void prepareFromUri(android.net.Uri, android.os.Bundle);
+ method public void removePlaylistItem(androidx.media.MediaItem2);
+ method public void replacePlaylistItem(int, androidx.media.MediaItem2);
+ method public void reset();
+ method public void rewind();
+ method public void seekTo(long);
+ method public void selectRoute(android.os.Bundle);
+ method public void sendCustomCommand(androidx.media.SessionCommand2, android.os.Bundle, android.os.ResultReceiver);
+ method public void setPlaybackSpeed(float);
+ method public void setPlaylist(java.util.List<androidx.media.MediaItem2>, androidx.media.MediaMetadata2);
+ method public void setRating(java.lang.String, androidx.media.Rating2);
+ method public void setRepeatMode(int);
+ method public void setShuffleMode(int);
+ method public void setVolumeTo(int, int);
+ method public void skipToNextItem();
+ method public void skipToPlaylistItem(androidx.media.MediaItem2);
+ method public void skipToPreviousItem();
+ method public void subscribeRoutesInfo();
+ method public void unsubscribeRoutesInfo();
+ method public void updatePlaylistMetadata(androidx.media.MediaMetadata2);
+ }
+
+ public static abstract class MediaController2.ControllerCallback {
+ ctor public MediaController2.ControllerCallback();
+ method public void onAllowedCommandsChanged(androidx.media.MediaController2, androidx.media.SessionCommandGroup2);
+ method public void onBufferingStateChanged(androidx.media.MediaController2, androidx.media.MediaItem2, int);
+ method public void onConnected(androidx.media.MediaController2, androidx.media.SessionCommandGroup2);
+ method public void onCurrentMediaItemChanged(androidx.media.MediaController2, androidx.media.MediaItem2);
+ method public void onCustomCommand(androidx.media.MediaController2, androidx.media.SessionCommand2, android.os.Bundle, android.os.ResultReceiver);
+ method public void onCustomLayoutChanged(androidx.media.MediaController2, java.util.List<androidx.media.MediaSession2.CommandButton>);
+ method public void onDisconnected(androidx.media.MediaController2);
+ method public void onError(androidx.media.MediaController2, int, android.os.Bundle);
+ method public void onPlaybackInfoChanged(androidx.media.MediaController2, androidx.media.MediaController2.PlaybackInfo);
+ method public void onPlaybackSpeedChanged(androidx.media.MediaController2, float);
+ method public void onPlayerStateChanged(androidx.media.MediaController2, int);
+ method public void onPlaylistChanged(androidx.media.MediaController2, java.util.List<androidx.media.MediaItem2>, androidx.media.MediaMetadata2);
+ method public void onPlaylistMetadataChanged(androidx.media.MediaController2, androidx.media.MediaMetadata2);
+ method public void onRepeatModeChanged(androidx.media.MediaController2, int);
+ method public void onRoutesInfoChanged(androidx.media.MediaController2, java.util.List<android.os.Bundle>);
+ method public void onSeekCompleted(androidx.media.MediaController2, long);
+ method public void onShuffleModeChanged(androidx.media.MediaController2, int);
+ }
+
+ public static final class MediaController2.PlaybackInfo {
+ method public androidx.media.AudioAttributesCompat getAudioAttributes();
+ method public int getControlType();
+ method public int getCurrentVolume();
+ method public int getMaxVolume();
+ method public int getPlaybackType();
+ field public static final int PLAYBACK_TYPE_LOCAL = 1; // 0x1
+ field public static final int PLAYBACK_TYPE_REMOTE = 2; // 0x2
+ }
+
+ public class MediaItem2 {
+ method public static androidx.media.MediaItem2 fromBundle(android.os.Bundle);
+ method public androidx.media.DataSourceDesc getDataSourceDesc();
+ method public int getFlags();
+ method public java.lang.String getMediaId();
+ method public androidx.media.MediaMetadata2 getMetadata();
+ method public boolean isBrowsable();
+ method public boolean isPlayable();
+ method public void setMetadata(androidx.media.MediaMetadata2);
+ method public android.os.Bundle toBundle();
+ field public static final int FLAG_BROWSABLE = 1; // 0x1
+ field public static final int FLAG_PLAYABLE = 2; // 0x2
+ }
+
+ public static final class MediaItem2.Builder {
+ ctor public MediaItem2.Builder(int);
+ method public androidx.media.MediaItem2 build();
+ method public androidx.media.MediaItem2.Builder setDataSourceDesc(androidx.media.DataSourceDesc);
+ method public androidx.media.MediaItem2.Builder setMediaId(java.lang.String);
+ method public androidx.media.MediaItem2.Builder setMetadata(androidx.media.MediaMetadata2);
+ }
+
+ public final class MediaMetadata2 {
+ method public boolean containsKey(java.lang.String);
+ method public static androidx.media.MediaMetadata2 fromBundle(android.os.Bundle);
+ method public android.graphics.Bitmap getBitmap(java.lang.String);
+ method public android.os.Bundle getExtras();
+ method public float getFloat(java.lang.String);
+ method public long getLong(java.lang.String);
+ method public java.lang.String getMediaId();
+ method public androidx.media.Rating2 getRating(java.lang.String);
+ method public java.lang.String getString(java.lang.String);
+ method public java.lang.CharSequence getText(java.lang.String);
+ method public java.util.Set<java.lang.String> keySet();
+ method public int size();
+ method public android.os.Bundle toBundle();
+ field public static final long BT_FOLDER_TYPE_ALBUMS = 2L; // 0x2L
+ field public static final long BT_FOLDER_TYPE_ARTISTS = 3L; // 0x3L
+ field public static final long BT_FOLDER_TYPE_GENRES = 4L; // 0x4L
+ field public static final long BT_FOLDER_TYPE_MIXED = 0L; // 0x0L
+ field public static final long BT_FOLDER_TYPE_PLAYLISTS = 5L; // 0x5L
+ field public static final long BT_FOLDER_TYPE_TITLES = 1L; // 0x1L
+ field public static final long BT_FOLDER_TYPE_YEARS = 6L; // 0x6L
+ field public static final java.lang.String METADATA_KEY_ADVERTISEMENT = "android.media.metadata.ADVERTISEMENT";
+ field public static final java.lang.String METADATA_KEY_ALBUM = "android.media.metadata.ALBUM";
+ field public static final java.lang.String METADATA_KEY_ALBUM_ART = "android.media.metadata.ALBUM_ART";
+ field public static final java.lang.String METADATA_KEY_ALBUM_ARTIST = "android.media.metadata.ALBUM_ARTIST";
+ field public static final java.lang.String METADATA_KEY_ALBUM_ART_URI = "android.media.metadata.ALBUM_ART_URI";
+ field public static final java.lang.String METADATA_KEY_ART = "android.media.metadata.ART";
+ field public static final java.lang.String METADATA_KEY_ARTIST = "android.media.metadata.ARTIST";
+ field public static final java.lang.String METADATA_KEY_ART_URI = "android.media.metadata.ART_URI";
+ field public static final java.lang.String METADATA_KEY_AUTHOR = "android.media.metadata.AUTHOR";
+ field public static final java.lang.String METADATA_KEY_BT_FOLDER_TYPE = "android.media.metadata.BT_FOLDER_TYPE";
+ field public static final java.lang.String METADATA_KEY_COMPILATION = "android.media.metadata.COMPILATION";
+ field public static final java.lang.String METADATA_KEY_COMPOSER = "android.media.metadata.COMPOSER";
+ field public static final java.lang.String METADATA_KEY_DATE = "android.media.metadata.DATE";
+ field public static final java.lang.String METADATA_KEY_DISC_NUMBER = "android.media.metadata.DISC_NUMBER";
+ field public static final java.lang.String METADATA_KEY_DISPLAY_DESCRIPTION = "android.media.metadata.DISPLAY_DESCRIPTION";
+ field public static final java.lang.String METADATA_KEY_DISPLAY_ICON = "android.media.metadata.DISPLAY_ICON";
+ field public static final java.lang.String METADATA_KEY_DISPLAY_ICON_URI = "android.media.metadata.DISPLAY_ICON_URI";
+ field public static final java.lang.String METADATA_KEY_DISPLAY_SUBTITLE = "android.media.metadata.DISPLAY_SUBTITLE";
+ field public static final java.lang.String METADATA_KEY_DISPLAY_TITLE = "android.media.metadata.DISPLAY_TITLE";
+ field public static final java.lang.String METADATA_KEY_DOWNLOAD_STATUS = "android.media.metadata.DOWNLOAD_STATUS";
+ field public static final java.lang.String METADATA_KEY_DURATION = "android.media.metadata.DURATION";
+ field public static final java.lang.String METADATA_KEY_EXTRAS = "android.media.metadata.EXTRAS";
+ field public static final java.lang.String METADATA_KEY_GENRE = "android.media.metadata.GENRE";
+ field public static final java.lang.String METADATA_KEY_MEDIA_ID = "android.media.metadata.MEDIA_ID";
+ field public static final java.lang.String METADATA_KEY_MEDIA_URI = "android.media.metadata.MEDIA_URI";
+ field public static final java.lang.String METADATA_KEY_NUM_TRACKS = "android.media.metadata.NUM_TRACKS";
+ field public static final java.lang.String METADATA_KEY_RATING = "android.media.metadata.RATING";
+ field public static final java.lang.String METADATA_KEY_TITLE = "android.media.metadata.TITLE";
+ field public static final java.lang.String METADATA_KEY_TRACK_NUMBER = "android.media.metadata.TRACK_NUMBER";
+ field public static final java.lang.String METADATA_KEY_USER_RATING = "android.media.metadata.USER_RATING";
+ field public static final java.lang.String METADATA_KEY_WRITER = "android.media.metadata.WRITER";
+ field public static final java.lang.String METADATA_KEY_YEAR = "android.media.metadata.YEAR";
+ field public static final long STATUS_DOWNLOADED = 2L; // 0x2L
+ field public static final long STATUS_DOWNLOADING = 1L; // 0x1L
+ field public static final long STATUS_NOT_DOWNLOADED = 0L; // 0x0L
+ }
+
+ public static final class MediaMetadata2.Builder {
+ ctor public MediaMetadata2.Builder();
+ ctor public MediaMetadata2.Builder(androidx.media.MediaMetadata2);
+ method public androidx.media.MediaMetadata2 build();
+ method public androidx.media.MediaMetadata2.Builder putBitmap(java.lang.String, android.graphics.Bitmap);
+ method public androidx.media.MediaMetadata2.Builder putFloat(java.lang.String, float);
+ method public androidx.media.MediaMetadata2.Builder putLong(java.lang.String, long);
+ method public androidx.media.MediaMetadata2.Builder putRating(java.lang.String, androidx.media.Rating2);
+ method public androidx.media.MediaMetadata2.Builder putString(java.lang.String, java.lang.String);
+ method public androidx.media.MediaMetadata2.Builder putText(java.lang.String, java.lang.CharSequence);
+ method public androidx.media.MediaMetadata2.Builder setExtras(android.os.Bundle);
+ }
+
+ public abstract class MediaPlayerBase implements java.lang.AutoCloseable {
+ ctor public MediaPlayerBase();
+ method public abstract androidx.media.AudioAttributesCompat getAudioAttributes();
+ method public long getBufferedPosition();
+ method public abstract int getBufferingState();
+ method public abstract androidx.media.DataSourceDesc getCurrentDataSource();
+ method public long getCurrentPosition();
+ method public long getDuration();
+ method public float getMaxPlayerVolume();
+ method public float getPlaybackSpeed();
+ method public abstract int getPlayerState();
+ method public abstract float getPlayerVolume();
+ method public boolean isReversePlaybackSupported();
+ method public abstract void loopCurrent(boolean);
+ method public abstract void pause();
+ method public abstract void play();
+ method public abstract void prepare();
+ method public abstract void registerPlayerEventCallback(java.util.concurrent.Executor, androidx.media.MediaPlayerBase.PlayerEventCallback);
+ method public abstract void reset();
+ method public abstract void seekTo(long);
+ method public abstract void setAudioAttributes(androidx.media.AudioAttributesCompat);
+ method public abstract void setDataSource(androidx.media.DataSourceDesc);
+ method public abstract void setNextDataSource(androidx.media.DataSourceDesc);
+ method public abstract void setNextDataSources(java.util.List<androidx.media.DataSourceDesc>);
+ method public abstract void setPlaybackSpeed(float);
+ method public abstract void setPlayerVolume(float);
+ method public abstract void skipToNext();
+ method public abstract void unregisterPlayerEventCallback(androidx.media.MediaPlayerBase.PlayerEventCallback);
+ field public static final int BUFFERING_STATE_BUFFERING_AND_PLAYABLE = 1; // 0x1
+ field public static final int BUFFERING_STATE_BUFFERING_AND_STARVED = 2; // 0x2
+ field public static final int BUFFERING_STATE_BUFFERING_COMPLETE = 3; // 0x3
+ field public static final int BUFFERING_STATE_UNKNOWN = 0; // 0x0
+ field public static final int PLAYER_STATE_ERROR = 3; // 0x3
+ field public static final int PLAYER_STATE_IDLE = 0; // 0x0
+ field public static final int PLAYER_STATE_PAUSED = 1; // 0x1
+ field public static final int PLAYER_STATE_PLAYING = 2; // 0x2
+ field public static final long UNKNOWN_TIME = -1L; // 0xffffffffffffffffL
+ }
+
+ public static abstract class MediaPlayerBase.PlayerEventCallback {
+ ctor public MediaPlayerBase.PlayerEventCallback();
+ method public void onBufferingStateChanged(androidx.media.MediaPlayerBase, androidx.media.DataSourceDesc, int);
+ method public void onCurrentDataSourceChanged(androidx.media.MediaPlayerBase, androidx.media.DataSourceDesc);
+ method public void onMediaPrepared(androidx.media.MediaPlayerBase, androidx.media.DataSourceDesc);
+ method public void onPlaybackSpeedChanged(androidx.media.MediaPlayerBase, float);
+ method public void onPlayerStateChanged(androidx.media.MediaPlayerBase, int);
+ method public void onSeekCompleted(androidx.media.MediaPlayerBase, long);
+ }
+
+ public abstract class MediaPlaylistAgent {
+ ctor public MediaPlaylistAgent();
+ method public abstract void addPlaylistItem(int, androidx.media.MediaItem2);
+ method public abstract androidx.media.MediaItem2 getCurrentMediaItem();
+ method public androidx.media.MediaItem2 getMediaItem(androidx.media.DataSourceDesc);
+ method public abstract java.util.List<androidx.media.MediaItem2> getPlaylist();
+ method public abstract androidx.media.MediaMetadata2 getPlaylistMetadata();
+ method public abstract int getRepeatMode();
+ method public abstract int getShuffleMode();
+ method public final void notifyPlaylistChanged();
+ method public final void notifyPlaylistMetadataChanged();
+ method public final void notifyRepeatModeChanged();
+ method public final void notifyShuffleModeChanged();
+ method public final void registerPlaylistEventCallback(java.util.concurrent.Executor, androidx.media.MediaPlaylistAgent.PlaylistEventCallback);
+ method public abstract void removePlaylistItem(androidx.media.MediaItem2);
+ method public abstract void replacePlaylistItem(int, androidx.media.MediaItem2);
+ method public abstract void setPlaylist(java.util.List<androidx.media.MediaItem2>, androidx.media.MediaMetadata2);
+ method public abstract void setRepeatMode(int);
+ method public abstract void setShuffleMode(int);
+ method public abstract void skipToNextItem();
+ method public abstract void skipToPlaylistItem(androidx.media.MediaItem2);
+ method public abstract void skipToPreviousItem();
+ method public final void unregisterPlaylistEventCallback(androidx.media.MediaPlaylistAgent.PlaylistEventCallback);
+ method public abstract void updatePlaylistMetadata(androidx.media.MediaMetadata2);
+ field public static final int REPEAT_MODE_ALL = 2; // 0x2
+ field public static final int REPEAT_MODE_GROUP = 3; // 0x3
+ field public static final int REPEAT_MODE_NONE = 0; // 0x0
+ field public static final int REPEAT_MODE_ONE = 1; // 0x1
+ field public static final int SHUFFLE_MODE_ALL = 1; // 0x1
+ field public static final int SHUFFLE_MODE_GROUP = 2; // 0x2
+ field public static final int SHUFFLE_MODE_NONE = 0; // 0x0
+ }
+
+ public static abstract class MediaPlaylistAgent.PlaylistEventCallback {
+ ctor public MediaPlaylistAgent.PlaylistEventCallback();
+ method public void onPlaylistChanged(androidx.media.MediaPlaylistAgent, java.util.List<androidx.media.MediaItem2>, androidx.media.MediaMetadata2);
+ method public void onPlaylistMetadataChanged(androidx.media.MediaPlaylistAgent, androidx.media.MediaMetadata2);
+ method public void onRepeatModeChanged(androidx.media.MediaPlaylistAgent, int);
+ method public void onShuffleModeChanged(androidx.media.MediaPlaylistAgent, int);
+ }
+
+ public class MediaSession2 implements java.lang.AutoCloseable {
+ method public void addPlaylistItem(int, androidx.media.MediaItem2);
+ method public void clearOnDataSourceMissingHelper();
+ method public void close();
+ method public long getBufferedPosition();
+ method public int getBufferingState();
+ method public java.util.List<androidx.media.MediaSession2.ControllerInfo> getConnectedControllers();
+ method public androidx.media.MediaItem2 getCurrentMediaItem();
+ method public long getCurrentPosition();
+ method public long getDuration();
+ method public float getPlaybackSpeed();
+ method public androidx.media.MediaPlayerBase getPlayer();
+ method public int getPlayerState();
+ method public java.util.List<androidx.media.MediaItem2> getPlaylist();
+ method public androidx.media.MediaPlaylistAgent getPlaylistAgent();
+ method public androidx.media.MediaMetadata2 getPlaylistMetadata();
+ method public int getRepeatMode();
+ method public int getShuffleMode();
+ method public androidx.media.SessionToken2 getToken();
+ method public androidx.media.VolumeProviderCompat getVolumeProvider();
+ method public void notifyError(int, android.os.Bundle);
+ method public void notifyRoutesInfoChanged(androidx.media.MediaSession2.ControllerInfo, java.util.List<android.os.Bundle>);
+ method public void pause();
+ method public void play();
+ method public void prepare();
+ method public void removePlaylistItem(androidx.media.MediaItem2);
+ method public void replacePlaylistItem(int, androidx.media.MediaItem2);
+ method public void reset();
+ method public void seekTo(long);
+ method public void sendCustomCommand(androidx.media.SessionCommand2, android.os.Bundle);
+ method public void sendCustomCommand(androidx.media.MediaSession2.ControllerInfo, androidx.media.SessionCommand2, android.os.Bundle, android.os.ResultReceiver);
+ method public void setAllowedCommands(androidx.media.MediaSession2.ControllerInfo, androidx.media.SessionCommandGroup2);
+ method public void setAudioFocusRequest(android.media.AudioFocusRequest);
+ method public void setCustomLayout(androidx.media.MediaSession2.ControllerInfo, java.util.List<androidx.media.MediaSession2.CommandButton>);
+ method public void setOnDataSourceMissingHelper(androidx.media.MediaSession2.OnDataSourceMissingHelper);
+ method public void setPlaybackSpeed(float);
+ method public void setPlaylist(java.util.List<androidx.media.MediaItem2>, androidx.media.MediaMetadata2);
+ method public void setRepeatMode(int);
+ method public void setShuffleMode(int);
+ method public void skipToNextItem();
+ method public void skipToPlaylistItem(androidx.media.MediaItem2);
+ method public void skipToPreviousItem();
+ method public void updatePlayer(androidx.media.MediaPlayerBase, androidx.media.MediaPlaylistAgent, androidx.media.VolumeProviderCompat);
+ method public void updatePlaylistMetadata(androidx.media.MediaMetadata2);
+ field public static final int ERROR_CODE_ACTION_ABORTED = 10; // 0xa
+ field public static final int ERROR_CODE_APP_ERROR = 1; // 0x1
+ field public static final int ERROR_CODE_AUTHENTICATION_EXPIRED = 3; // 0x3
+ field public static final int ERROR_CODE_CONCURRENT_STREAM_LIMIT = 5; // 0x5
+ field public static final int ERROR_CODE_CONTENT_ALREADY_PLAYING = 8; // 0x8
+ field public static final int ERROR_CODE_END_OF_QUEUE = 11; // 0xb
+ field public static final int ERROR_CODE_NOT_AVAILABLE_IN_REGION = 7; // 0x7
+ field public static final int ERROR_CODE_NOT_SUPPORTED = 2; // 0x2
+ field public static final int ERROR_CODE_PARENTAL_CONTROL_RESTRICTED = 6; // 0x6
+ field public static final int ERROR_CODE_PREMIUM_ACCOUNT_REQUIRED = 4; // 0x4
+ field public static final int ERROR_CODE_SETUP_REQUIRED = 12; // 0xc
+ field public static final int ERROR_CODE_SKIP_LIMIT_REACHED = 9; // 0x9
+ field public static final int ERROR_CODE_UNKNOWN_ERROR = 0; // 0x0
+ }
+
+ public static final class MediaSession2.Builder {
+ ctor public MediaSession2.Builder(android.content.Context);
+ method public androidx.media.MediaSession2 build();
+ method public androidx.media.MediaSession2.Builder setId(java.lang.String);
+ method public androidx.media.MediaSession2.Builder setPlayer(androidx.media.MediaPlayerBase);
+ method public androidx.media.MediaSession2.Builder setPlaylistAgent(androidx.media.MediaPlaylistAgent);
+ method public androidx.media.MediaSession2.Builder setSessionActivity(android.app.PendingIntent);
+ method public androidx.media.MediaSession2.Builder setSessionCallback(java.util.concurrent.Executor, androidx.media.MediaSession2.SessionCallback);
+ method public androidx.media.MediaSession2.Builder setVolumeProvider(androidx.media.VolumeProviderCompat);
+ }
+
+ public static final class MediaSession2.CommandButton {
+ method public androidx.media.SessionCommand2 getCommand();
+ method public java.lang.String getDisplayName();
+ method public android.os.Bundle getExtras();
+ method public int getIconResId();
+ method public boolean isEnabled();
+ }
+
+ public static final class MediaSession2.CommandButton.Builder {
+ ctor public MediaSession2.CommandButton.Builder();
+ method public androidx.media.MediaSession2.CommandButton build();
+ method public androidx.media.MediaSession2.CommandButton.Builder setCommand(androidx.media.SessionCommand2);
+ method public androidx.media.MediaSession2.CommandButton.Builder setDisplayName(java.lang.String);
+ method public androidx.media.MediaSession2.CommandButton.Builder setEnabled(boolean);
+ method public androidx.media.MediaSession2.CommandButton.Builder setExtras(android.os.Bundle);
+ method public androidx.media.MediaSession2.CommandButton.Builder setIconResId(int);
+ }
+
+ public static final class MediaSession2.ControllerInfo {
+ method public java.lang.String getPackageName();
+ method public int getUid();
+ }
+
+ public static abstract interface MediaSession2.OnDataSourceMissingHelper {
+ method public abstract androidx.media.DataSourceDesc onDataSourceMissing(androidx.media.MediaSession2, androidx.media.MediaItem2);
+ }
+
+ public static abstract class MediaSession2.SessionCallback {
+ ctor public MediaSession2.SessionCallback();
+ method public void onBufferingStateChanged(androidx.media.MediaSession2, androidx.media.MediaPlayerBase, androidx.media.MediaItem2, int);
+ method public boolean onCommandRequest(androidx.media.MediaSession2, androidx.media.MediaSession2.ControllerInfo, androidx.media.SessionCommand2);
+ method public androidx.media.SessionCommandGroup2 onConnect(androidx.media.MediaSession2, androidx.media.MediaSession2.ControllerInfo);
+ method public void onCurrentMediaItemChanged(androidx.media.MediaSession2, androidx.media.MediaPlayerBase, androidx.media.MediaItem2);
+ method public void onCustomCommand(androidx.media.MediaSession2, androidx.media.MediaSession2.ControllerInfo, androidx.media.SessionCommand2, android.os.Bundle, android.os.ResultReceiver);
+ method public void onDisconnected(androidx.media.MediaSession2, androidx.media.MediaSession2.ControllerInfo);
+ method public void onFastForward(androidx.media.MediaSession2, androidx.media.MediaSession2.ControllerInfo);
+ method public void onMediaPrepared(androidx.media.MediaSession2, androidx.media.MediaPlayerBase, androidx.media.MediaItem2);
+ method public void onPlayFromMediaId(androidx.media.MediaSession2, androidx.media.MediaSession2.ControllerInfo, java.lang.String, android.os.Bundle);
+ method public void onPlayFromSearch(androidx.media.MediaSession2, androidx.media.MediaSession2.ControllerInfo, java.lang.String, android.os.Bundle);
+ method public void onPlayFromUri(androidx.media.MediaSession2, androidx.media.MediaSession2.ControllerInfo, android.net.Uri, android.os.Bundle);
+ method public void onPlaybackSpeedChanged(androidx.media.MediaSession2, androidx.media.MediaPlayerBase, float);
+ method public void onPlayerStateChanged(androidx.media.MediaSession2, androidx.media.MediaPlayerBase, int);
+ method public void onPlaylistChanged(androidx.media.MediaSession2, androidx.media.MediaPlaylistAgent, java.util.List<androidx.media.MediaItem2>, androidx.media.MediaMetadata2);
+ method public void onPlaylistMetadataChanged(androidx.media.MediaSession2, androidx.media.MediaPlaylistAgent, androidx.media.MediaMetadata2);
+ method public void onPrepareFromMediaId(androidx.media.MediaSession2, androidx.media.MediaSession2.ControllerInfo, java.lang.String, android.os.Bundle);
+ method public void onPrepareFromSearch(androidx.media.MediaSession2, androidx.media.MediaSession2.ControllerInfo, java.lang.String, android.os.Bundle);
+ method public void onPrepareFromUri(androidx.media.MediaSession2, androidx.media.MediaSession2.ControllerInfo, android.net.Uri, android.os.Bundle);
+ method public void onRepeatModeChanged(androidx.media.MediaSession2, androidx.media.MediaPlaylistAgent, int);
+ method public void onRewind(androidx.media.MediaSession2, androidx.media.MediaSession2.ControllerInfo);
+ method public void onSeekCompleted(androidx.media.MediaSession2, androidx.media.MediaPlayerBase, long);
+ method public void onSelectRoute(androidx.media.MediaSession2, androidx.media.MediaSession2.ControllerInfo, android.os.Bundle);
+ method public void onSetRating(androidx.media.MediaSession2, androidx.media.MediaSession2.ControllerInfo, java.lang.String, androidx.media.Rating2);
+ method public void onShuffleModeChanged(androidx.media.MediaSession2, androidx.media.MediaPlaylistAgent, int);
+ method public void onSubscribeRoutesInfo(androidx.media.MediaSession2, androidx.media.MediaSession2.ControllerInfo);
+ method public void onUnsubscribeRoutesInfo(androidx.media.MediaSession2, androidx.media.MediaSession2.ControllerInfo);
+ }
+
+ public final class Rating2 {
+ method public static androidx.media.Rating2 fromBundle(android.os.Bundle);
+ method public float getPercentRating();
+ method public int getRatingStyle();
+ method public float getStarRating();
+ method public boolean hasHeart();
+ method public boolean isRated();
+ method public boolean isThumbUp();
+ method public static androidx.media.Rating2 newHeartRating(boolean);
+ method public static androidx.media.Rating2 newPercentageRating(float);
+ method public static androidx.media.Rating2 newStarRating(int, float);
+ method public static androidx.media.Rating2 newThumbRating(boolean);
+ method public static androidx.media.Rating2 newUnratedRating(int);
+ method public android.os.Bundle toBundle();
+ field public static final int RATING_3_STARS = 3; // 0x3
+ field public static final int RATING_4_STARS = 4; // 0x4
+ field public static final int RATING_5_STARS = 5; // 0x5
+ field public static final int RATING_HEART = 1; // 0x1
+ field public static final int RATING_NONE = 0; // 0x0
+ field public static final int RATING_PERCENTAGE = 6; // 0x6
+ field public static final int RATING_THUMB_UP_DOWN = 2; // 0x2
+ }
+
+ public final class SessionCommand2 {
+ ctor public SessionCommand2(int);
+ ctor public SessionCommand2(java.lang.String, android.os.Bundle);
+ method public int getCommandCode();
+ method public java.lang.String getCustomCommand();
+ method public android.os.Bundle getExtras();
+ field public static final int COMMAND_CODE_CUSTOM = 0; // 0x0
+ field public static final int COMMAND_CODE_PLAYBACK_PAUSE = 2; // 0x2
+ field public static final int COMMAND_CODE_PLAYBACK_PLAY = 1; // 0x1
+ field public static final int COMMAND_CODE_PLAYBACK_PREPARE = 6; // 0x6
+ field public static final int COMMAND_CODE_PLAYBACK_RESET = 3; // 0x3
+ field public static final int COMMAND_CODE_PLAYBACK_SEEK_TO = 9; // 0x9
+ field public static final int COMMAND_CODE_PLAYBACK_SET_SPEED = 39; // 0x27
+ field public static final int COMMAND_CODE_PLAYLIST_ADD_ITEM = 15; // 0xf
+ field public static final int COMMAND_CODE_PLAYLIST_GET_CURRENT_MEDIA_ITEM = 20; // 0x14
+ field public static final int COMMAND_CODE_PLAYLIST_GET_LIST = 18; // 0x12
+ field public static final int COMMAND_CODE_PLAYLIST_GET_LIST_METADATA = 20; // 0x14
+ field public static final int COMMAND_CODE_PLAYLIST_REMOVE_ITEM = 16; // 0x10
+ field public static final int COMMAND_CODE_PLAYLIST_REPLACE_ITEM = 17; // 0x11
+ field public static final int COMMAND_CODE_PLAYLIST_SET_LIST = 19; // 0x13
+ field public static final int COMMAND_CODE_PLAYLIST_SET_LIST_METADATA = 21; // 0x15
+ field public static final int COMMAND_CODE_PLAYLIST_SET_REPEAT_MODE = 14; // 0xe
+ field public static final int COMMAND_CODE_PLAYLIST_SET_SHUFFLE_MODE = 13; // 0xd
+ field public static final int COMMAND_CODE_PLAYLIST_SKIP_TO_NEXT_ITEM = 4; // 0x4
+ field public static final int COMMAND_CODE_PLAYLIST_SKIP_TO_PLAYLIST_ITEM = 12; // 0xc
+ field public static final int COMMAND_CODE_PLAYLIST_SKIP_TO_PREV_ITEM = 5; // 0x5
+ field public static final int COMMAND_CODE_SESSION_FAST_FORWARD = 7; // 0x7
+ field public static final int COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID = 22; // 0x16
+ field public static final int COMMAND_CODE_SESSION_PLAY_FROM_SEARCH = 24; // 0x18
+ field public static final int COMMAND_CODE_SESSION_PLAY_FROM_URI = 23; // 0x17
+ field public static final int COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID = 25; // 0x19
+ field public static final int COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH = 27; // 0x1b
+ field public static final int COMMAND_CODE_SESSION_PREPARE_FROM_URI = 26; // 0x1a
+ field public static final int COMMAND_CODE_SESSION_REWIND = 8; // 0x8
+ field public static final int COMMAND_CODE_SESSION_SELECT_ROUTE = 38; // 0x26
+ field public static final int COMMAND_CODE_SESSION_SET_RATING = 28; // 0x1c
+ field public static final int COMMAND_CODE_SESSION_SUBSCRIBE_ROUTES_INFO = 36; // 0x24
+ field public static final int COMMAND_CODE_SESSION_UNSUBSCRIBE_ROUTES_INFO = 37; // 0x25
+ field public static final int COMMAND_CODE_VOLUME_ADJUST_VOLUME = 11; // 0xb
+ field public static final int COMMAND_CODE_VOLUME_SET_VOLUME = 10; // 0xa
+ }
+
+ public final class SessionCommandGroup2 {
+ ctor public SessionCommandGroup2();
+ ctor public SessionCommandGroup2(androidx.media.SessionCommandGroup2);
+ method public void addAllPredefinedCommands();
+ method public void addCommand(androidx.media.SessionCommand2);
+ method public void addCommand(int);
+ method public java.util.Set<androidx.media.SessionCommand2> getCommands();
+ method public boolean hasCommand(androidx.media.SessionCommand2);
+ method public boolean hasCommand(int);
+ method public void removeCommand(androidx.media.SessionCommand2);
+ method public void removeCommand(int);
+ }
+
+ public final class SessionToken2 {
+ method public static androidx.media.SessionToken2 fromBundle(android.os.Bundle);
+ method public java.lang.String getId();
+ method public java.lang.String getPackageName();
+ method public java.lang.String getServiceName();
+ method public int getType();
+ method public int getUid();
+ method public android.os.Bundle toBundle();
+ field public static final int TYPE_SESSION = 0; // 0x0
+ }
+
public abstract class VolumeProviderCompat {
ctor public VolumeProviderCompat(int, int, int);
method public final int getCurrentVolume();
diff --git a/media/src/androidTest/java/androidx/media/MediaPlayer2Test.java b/media/src/androidTest/java/androidx/media/MediaPlayer2Test.java
index cc409b7..04cc270 100644
--- a/media/src/androidTest/java/androidx/media/MediaPlayer2Test.java
+++ b/media/src/androidTest/java/androidx/media/MediaPlayer2Test.java
@@ -29,6 +29,7 @@
import android.media.MediaRecorder;
import android.media.MediaTimestamp;
import android.media.PlaybackParams;
+import android.media.SubtitleData;
import android.media.SyncParams;
import android.media.audiofx.AudioEffect;
import android.media.audiofx.Visualizer;
@@ -36,12 +37,15 @@
import android.os.Build;
import android.os.Environment;
import android.support.test.filters.LargeTest;
+import android.support.test.filters.MediumTest;
import android.support.test.filters.SdkSuppress;
import android.support.test.runner.AndroidJUnit4;
import android.util.Log;
import androidx.media.test.R;
+import org.junit.After;
+import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -52,9 +56,11 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Vector;
+import java.util.concurrent.BlockingDeque;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
+import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
@@ -77,14 +83,16 @@
private File mOutFile;
private Camera mCamera;
+ @Before
@Override
- public void setUp() throws Exception {
+ public void setUp() throws Throwable {
super.setUp();
mRecordedFilePath = new File(Environment.getExternalStorageDirectory(),
"mediaplayer_record.out").getAbsolutePath();
mOutFile = new File(mRecordedFilePath);
}
+ @After
@Override
public void tearDown() throws Exception {
super.tearDown();
@@ -93,61 +101,8 @@
}
}
- // Bug 13652927
- public void testVorbisCrash() throws Exception {
- MediaPlayer2 mp = mPlayer;
- MediaPlayer2 mp2 = mPlayer2;
- AssetFileDescriptor afd2 = mResources.openRawResourceFd(R.raw.testmp3_2);
- mp2.setDataSource(new DataSourceDesc.Builder()
- .setDataSource(afd2.getFileDescriptor(), afd2.getStartOffset(), afd2.getLength())
- .build());
- final Monitor onPrepareCalled = new Monitor();
- final Monitor onErrorCalled = new Monitor();
- MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
- @Override
- public void onInfo(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
- if (what == MediaPlayer2.MEDIA_INFO_PREPARED) {
- onPrepareCalled.signal();
- }
- }
-
- @Override
- public void onError(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
- onErrorCalled.signal();
- }
- };
- mp2.setMediaPlayer2EventCallback(mExecutor, ecb);
- mp2.prepare();
- onPrepareCalled.waitForSignal();
- afd2.close();
- mp2.clearMediaPlayer2EventCallback();
-
- mp2.loopCurrent(true);
- mp2.play();
-
- for (int i = 0; i < 20; i++) {
- try {
- AssetFileDescriptor afd = mResources.openRawResourceFd(R.raw.bug13652927);
- mp.setDataSource(new DataSourceDesc.Builder()
- .setDataSource(afd.getFileDescriptor(), afd.getStartOffset(),
- afd.getLength())
- .build());
- mp.setMediaPlayer2EventCallback(mExecutor, ecb);
- onPrepareCalled.reset();
- mp.prepare();
- onErrorCalled.waitForSignal();
- afd.close();
- } catch (Exception e) {
- // expected to fail
- Log.i("@@@", "failed: " + e);
- }
- Thread.sleep(500);
- assertTrue("media player died",
- mp2.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
- mp.reset();
- }
- }
-
+ @Test
+ @MediumTest
public void testPlayNullSourcePath() throws Exception {
final Monitor onSetDataSourceCalled = new Monitor();
MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
@@ -168,6 +123,8 @@
onSetDataSourceCalled.waitForSignal();
}
+ @Test
+ @LargeTest
public void testPlayAudioFromDataURI() throws Exception {
final int mp3Duration = 34909;
final int tolerance = 70;
@@ -216,9 +173,6 @@
.setLegacyStreamType(AudioManager.STREAM_MUSIC)
.build();
mp.setAudioAttributes(attributes);
- /* FIXME: ensure screen is on while testing.
- mp.setWakeMode(mContext, PowerManager.PARTIAL_WAKE_LOCK);
- */
assertFalse(mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
onPlayCalled.reset();
@@ -280,6 +234,8 @@
}
}
+ @Test
+ @LargeTest
public void testPlayAudio() throws Exception {
final int resid = R.raw.testmp3_2;
final int mp3Duration = 34909;
@@ -410,7 +366,6 @@
.setInternalLegacyStreamType(AudioManager.STREAM_MUSIC)
.build();
mp.setAudioAttributes(attributes);
- mp.setWakeMode(mContext, PowerManager.PARTIAL_WAKE_LOCK);
assertFalse(mp.isPlaying());
onPlayCalled.reset();
@@ -441,6 +396,8 @@
}
*/
+ @Test
+ @LargeTest
public void testPlayAudioLooping() throws Exception {
final int resid = R.raw.testmp3;
@@ -458,8 +415,10 @@
@Override
public void onInfo(MediaPlayer2 mp, DataSourceDesc dsd,
int what, int extra) {
- Log.i("@@@", "got oncompletion");
- onCompletionCalled.signal();
+ if (what == MediaPlayer2.MEDIA_INFO_PLAYBACK_COMPLETE) {
+ Log.i("@@@", "got oncompletion");
+ onCompletionCalled.signal();
+ }
}
@Override
@@ -496,6 +455,8 @@
}
}
+ @Test
+ @LargeTest
public void testPlayMidi() throws Exception {
final int resid = R.raw.midi8sec;
final int midiDuration = 8000;
@@ -1448,14 +1409,14 @@
}
}
+ @Test
+ @LargeTest
public void testDeselectTrackForSubtitleTracks() throws Throwable {
if (!checkLoadResource(R.raw.testvideo_with_2_subtitle_tracks)) {
return; // skip;
}
- /* FIXME: find out counter part of waitForIdleSync.
- getInstrumentation().waitForIdleSync();
- */
+ mInstrumentation.waitForIdleSync();
MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
@Override
@@ -1478,21 +1439,18 @@
mOnDeselectTrackCalled.signal();
}
}
- };
- synchronized (mEventCbLock) {
- mEventCallbacks.add(ecb);
- }
- /* TODO: uncomment once API is available in supportlib.
- mPlayer.setOnSubtitleDataListener(new MediaPlayer2.OnSubtitleDataListener() {
@Override
- public void onSubtitleData(MediaPlayer2 mp, SubtitleData data) {
+ public void onSubtitleData(
+ MediaPlayer2 mp, DataSourceDesc dsd, SubtitleData data) {
if (data != null && data.getData() != null) {
mOnSubtitleDataCalled.signal();
}
}
- });
- */
+ };
+ synchronized (mEventCbLock) {
+ mEventCallbacks.add(ecb);
+ }
mPlayer.setSurface(mActivity.getSurfaceHolder().getSurface());
@@ -1535,22 +1493,13 @@
mPlayer.reset();
}
+ @Test
+ @LargeTest
public void testChangeSubtitleTrack() throws Throwable {
if (!checkLoadResource(R.raw.testvideo_with_2_subtitle_tracks)) {
return; // skip;
}
- /* TODO: uncomment once API is available in supportlib.
- mPlayer.setOnSubtitleDataListener(new MediaPlayer2.OnSubtitleDataListener() {
- @Override
- public void onSubtitleData(MediaPlayer2 mp, SubtitleData data) {
- if (data != null && data.getData() != null) {
- mOnSubtitleDataCalled.signal();
- }
- }
- });
- */
-
MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
@Override
public void onInfo(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
@@ -1567,6 +1516,14 @@
mOnPlayCalled.signal();
}
}
+
+ @Override
+ public void onSubtitleData(
+ MediaPlayer2 mp, DataSourceDesc dsd, SubtitleData data) {
+ if (data != null && data.getData() != null) {
+ mOnSubtitleDataCalled.signal();
+ }
+ }
};
synchronized (mEventCbLock) {
mEventCallbacks.add(ecb);
@@ -1656,10 +1613,77 @@
mPlayer.reset();
}
+ @Test
+ @LargeTest
+ public void testMediaTimeDiscontinuity() throws Exception {
+ if (!checkLoadResource(
+ R.raw.bbb_s1_320x240_mp4_h264_mp2_800kbps_30fps_aac_lc_5ch_240kbps_44100hz)) {
+ return; // skip
+ }
+
+ final BlockingDeque<MediaTimestamp> timestamps = new LinkedBlockingDeque<>();
+ MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
+ @Override
+ public void onCallCompleted(MediaPlayer2 mp, DataSourceDesc dsd, int what, int status) {
+ if (what == MediaPlayer2.CALL_COMPLETED_SEEK_TO) {
+ mOnSeekCompleteCalled.signal();
+ }
+ }
+ @Override
+ public void onMediaTimeDiscontinuity(
+ MediaPlayer2 mp, DataSourceDesc dsd, MediaTimestamp timestamp) {
+ timestamps.add(timestamp);
+ mOnMediaTimeDiscontinuityCalled.signal();
+ }
+ };
+ synchronized (mEventCbLock) {
+ mEventCallbacks.add(ecb);
+ }
+
+ mPlayer.setSurface(mActivity.getSurfaceHolder().getSurface());
+ mPlayer.prepare();
+
+ // Timestamp needs to be reported when playback starts.
+ mOnMediaTimeDiscontinuityCalled.reset();
+ mPlayer.play();
+ do {
+ assertTrue(mOnMediaTimeDiscontinuityCalled.waitForSignal(1000));
+ } while (Math.abs(timestamps.getLast().getMediaClockRate() - 1.0f) > 0.01f);
+
+ // Timestamp needs to be reported when seeking is done.
+ mOnSeekCompleteCalled.reset();
+ mOnMediaTimeDiscontinuityCalled.reset();
+ mPlayer.seekTo(3000);
+ mOnSeekCompleteCalled.waitForSignal();
+ do {
+ assertTrue(mOnMediaTimeDiscontinuityCalled.waitForSignal(1000));
+ } while (Math.abs(timestamps.getLast().getMediaClockRate() - 1.0f) > 0.01f);
+
+ // Timestamp needs to be updated when playback rate changes.
+ mOnMediaTimeDiscontinuityCalled.reset();
+ mPlayer.setPlaybackParams(new PlaybackParams().setSpeed(0.5f));
+ mOnMediaTimeDiscontinuityCalled.waitForSignal();
+ do {
+ assertTrue(mOnMediaTimeDiscontinuityCalled.waitForSignal(1000));
+ } while (Math.abs(timestamps.getLast().getMediaClockRate() - 0.5f) > 0.01f);
+
+ // Timestamp needs to be updated when player is paused.
+ mOnMediaTimeDiscontinuityCalled.reset();
+ mPlayer.pause();
+ mOnMediaTimeDiscontinuityCalled.waitForSignal();
+ do {
+ assertTrue(mOnMediaTimeDiscontinuityCalled.waitForSignal(1000));
+ } while (Math.abs(timestamps.getLast().getMediaClockRate() - 0.0f) > 0.01f);
+
+ mPlayer.reset();
+ }
+
/*
* This test assumes the resources being tested are between 8 and 14 seconds long
* The ones being used here are 10 seconds long.
*/
+ @Test
+ @LargeTest
public void testResumeAtEnd() throws Throwable {
int testsRun = testResumeAtEnd(R.raw.loudsoftmp3)
+ testResumeAtEnd(R.raw.loudsoftwav)
diff --git a/media/src/androidTest/java/androidx/media/MediaPlayer2TestBase.java b/media/src/androidTest/java/androidx/media/MediaPlayer2TestBase.java
index 34c464b..41fef64 100644
--- a/media/src/androidTest/java/androidx/media/MediaPlayer2TestBase.java
+++ b/media/src/androidTest/java/androidx/media/MediaPlayer2TestBase.java
@@ -15,20 +15,31 @@
*/
package androidx.media;
+import static android.content.Context.KEYGUARD_SERVICE;
+
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
+import android.app.Instrumentation;
+import android.app.KeyguardManager;
import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.content.res.Resources;
import android.media.AudioManager;
import android.media.MediaTimestamp;
+import android.media.SubtitleData;
import android.media.TimedMetaData;
import android.net.Uri;
+import android.os.PersistableBundle;
+import android.os.PowerManager;
+import android.support.test.InstrumentationRegistry;
import android.support.test.rule.ActivityTestRule;
import android.view.SurfaceHolder;
+import android.view.WindowManager;
+
+import androidx.annotation.CallSuper;
import org.junit.After;
import org.junit.Before;
@@ -39,6 +50,7 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Logger;
@@ -63,6 +75,7 @@
protected Monitor mOnCompletionCalled = new Monitor();
protected Monitor mOnInfoCalled = new Monitor();
protected Monitor mOnErrorCalled = new Monitor();
+ protected Monitor mOnMediaTimeDiscontinuityCalled = new Monitor();
protected int mCallStatus;
protected Context mContext;
@@ -73,34 +86,35 @@
protected MediaPlayer2 mPlayer = null;
protected MediaPlayer2 mPlayer2 = null;
protected MediaStubActivity mActivity;
+ protected Instrumentation mInstrumentation;
protected final Object mEventCbLock = new Object();
- protected List<MediaPlayer2.MediaPlayer2EventCallback> mEventCallbacks =
- new ArrayList<MediaPlayer2.MediaPlayer2EventCallback>();
+ protected List<MediaPlayer2.MediaPlayer2EventCallback> mEventCallbacks = new ArrayList<>();
protected final Object mEventCbLock2 = new Object();
- protected List<MediaPlayer2.MediaPlayer2EventCallback> mEventCallbacks2 =
- new ArrayList<MediaPlayer2.MediaPlayer2EventCallback>();
+ protected List<MediaPlayer2.MediaPlayer2EventCallback> mEventCallbacks2 = new ArrayList<>();
@Rule
public ActivityTestRule<MediaStubActivity> mActivityRule =
new ActivityTestRule<>(MediaStubActivity.class);
+ public PowerManager.WakeLock mScreenLock;
+ private KeyguardManager mKeyguardManager;
// convenience functions to create MediaPlayer2
- protected static MediaPlayer2 createMediaPlayer2(Context context, Uri uri) {
+ protected MediaPlayer2 createMediaPlayer2(Context context, Uri uri) {
return createMediaPlayer2(context, uri, null);
}
- protected static MediaPlayer2 createMediaPlayer2(Context context, Uri uri,
+ protected MediaPlayer2 createMediaPlayer2(Context context, Uri uri,
SurfaceHolder holder) {
AudioManager am = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
int s = am.generateAudioSessionId();
return createMediaPlayer2(context, uri, holder, null, s > 0 ? s : 0);
}
- protected static MediaPlayer2 createMediaPlayer2(Context context, Uri uri, SurfaceHolder holder,
+ protected MediaPlayer2 createMediaPlayer2(Context context, Uri uri, SurfaceHolder holder,
AudioAttributesCompat audioAttributes, int audioSessionId) {
try {
- MediaPlayer2 mp = MediaPlayer2.create();
+ MediaPlayer2 mp = createMediaPlayer2OnUiThread();
final AudioAttributesCompat aa = audioAttributes != null ? audioAttributes :
new AudioAttributesCompat.Builder().build();
mp.setAudioAttributes(aa);
@@ -142,13 +156,13 @@
return null;
}
- protected static MediaPlayer2 createMediaPlayer2(Context context, int resid) {
+ protected MediaPlayer2 createMediaPlayer2(Context context, int resid) {
AudioManager am = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
int s = am.generateAudioSessionId();
return createMediaPlayer2(context, resid, null, s > 0 ? s : 0);
}
- protected static MediaPlayer2 createMediaPlayer2(Context context, int resid,
+ protected MediaPlayer2 createMediaPlayer2(Context context, int resid,
AudioAttributesCompat audioAttributes, int audioSessionId) {
try {
AssetFileDescriptor afd = context.getResources().openRawResourceFd(resid);
@@ -156,7 +170,7 @@
return null;
}
- MediaPlayer2 mp = MediaPlayer2.create();
+ MediaPlayer2 mp = createMediaPlayer2OnUiThread();
final AudioAttributesCompat aa = audioAttributes != null ? audioAttributes :
new AudioAttributesCompat.Builder().build();
@@ -202,6 +216,20 @@
return null;
}
+ private MediaPlayer2 createMediaPlayer2OnUiThread() {
+ final MediaPlayer2[] mp = new MediaPlayer2[1];
+ try {
+ mActivityRule.runOnUiThread(new Runnable() {
+ public void run() {
+ mp[0] = MediaPlayer2.create();
+ }
+ });
+ } catch (Throwable throwable) {
+ fail("Failed to create MediaPlayer2 instance on UI thread.");
+ }
+ return mp[0];
+ }
+
public static class Monitor {
private int mNumSignal;
@@ -255,8 +283,24 @@
}
@Before
- public void setUp() throws Exception {
+ @CallSuper
+ public void setUp() throws Throwable {
+ mInstrumentation = InstrumentationRegistry.getInstrumentation();
+ mKeyguardManager = (KeyguardManager)
+ mInstrumentation.getTargetContext().getSystemService(KEYGUARD_SERVICE);
mActivity = mActivityRule.getActivity();
+ mActivityRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ // Keep screen on while testing.
+ mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ mActivity.setTurnScreenOn(true);
+ mActivity.setShowWhenLocked(true);
+ mKeyguardManager.requestDismissKeyguard(mActivity, null);
+ }
+ });
+ mInstrumentation.waitForIdleSync();
+
try {
mActivityRule.runOnUiThread(new Runnable() {
public void run() {
@@ -277,6 +321,7 @@
}
@After
+ @CallSuper
public void tearDown() throws Exception {
if (mPlayer != null) {
mPlayer.close();
@@ -340,11 +385,11 @@
}
@Override
- public void onMediaTimeChanged(MediaPlayer2 mp, DataSourceDesc dsd,
+ public void onMediaTimeDiscontinuity(MediaPlayer2 mp, DataSourceDesc dsd,
MediaTimestamp timestamp) {
synchronized (cbLock) {
for (MediaPlayer2.MediaPlayer2EventCallback ecb : ecbs) {
- ecb.onMediaTimeChanged(mp, dsd, timestamp);
+ ecb.onMediaTimeDiscontinuity(mp, dsd, timestamp);
}
}
}
@@ -357,6 +402,15 @@
}
}
}
+ @Override
+ public void onSubtitleData(MediaPlayer2 mp, DataSourceDesc dsd,
+ final SubtitleData data) {
+ synchronized (cbLock) {
+ for (MediaPlayer2.MediaPlayer2EventCallback ecb : ecbs) {
+ ecb.onSubtitleData(mp, dsd, data);
+ }
+ }
+ }
});
}
@@ -486,11 +540,7 @@
boolean audioOnly = (width != null && width.intValue() == -1)
|| (height != null && height.intValue() == -1);
-
mPlayer.setSurface(mActivity.getSurfaceHolder().getSurface());
- /* FIXME: ensure that screen is on in activity level.
- mPlayer.setScreenOnWhilePlaying(true);
- */
synchronized (mEventCbLock) {
mEventCallbacks.add(new MediaPlayer2.MediaPlayer2EventCallback() {
@@ -562,6 +612,42 @@
Thread.sleep(playTime);
}
+ // validate a few MediaMetrics.
+ PersistableBundle metrics = mPlayer.getMetrics();
+ if (metrics == null) {
+ fail("MediaPlayer.getMetrics() returned null metrics");
+ } else if (metrics.isEmpty()) {
+ fail("MediaPlayer.getMetrics() returned empty metrics");
+ } else {
+
+ int size = metrics.size();
+ Set<String> keys = metrics.keySet();
+
+ if (keys == null) {
+ fail("MediaMetricsSet returned no keys");
+ } else if (keys.size() != size) {
+ fail("MediaMetricsSet.keys().size() mismatch MediaMetricsSet.size()");
+ }
+
+ // we played something; so one of these should be non-null
+ String vmime = metrics.getString(MediaPlayer2.MetricsConstants.MIME_TYPE_VIDEO, null);
+ String amime = metrics.getString(MediaPlayer2.MetricsConstants.MIME_TYPE_AUDIO, null);
+ if (vmime == null && amime == null) {
+ fail("getMetrics() returned neither video nor audio mime value");
+ }
+
+ long duration = metrics.getLong(MediaPlayer2.MetricsConstants.DURATION, -2);
+ if (duration == -2) {
+ fail("getMetrics() didn't return a duration");
+ }
+ long playing = metrics.getLong(MediaPlayer2.MetricsConstants.PLAYING, -2);
+ if (playing == -2) {
+ fail("getMetrics() didn't return a playing time");
+ }
+ if (!keys.contains(MediaPlayer2.MetricsConstants.PLAYING)) {
+ fail("MediaMetricsSet.keys() missing: " + MediaPlayer2.MetricsConstants.PLAYING);
+ }
+ }
mPlayer.reset();
}
diff --git a/media/src/main/java/androidx/media/DataSourceDesc.java b/media/src/main/java/androidx/media/DataSourceDesc.java
index 7a4fe5c..f76f651 100644
--- a/media/src/main/java/androidx/media/DataSourceDesc.java
+++ b/media/src/main/java/androidx/media/DataSourceDesc.java
@@ -16,14 +16,11 @@
package androidx.media;
-import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.annotation.RestrictTo;
import androidx.core.util.Preconditions;
import java.io.FileDescriptor;
@@ -36,16 +33,12 @@
import java.util.Map;
/**
- * @hide
- * Structure for data source descriptor.
+ * Structure for data source descriptor. Used by {@link MediaItem2}.
+ * <p>
+ * Users should use {@link Builder} to change {@link DataSourceDesc}.
*
- * Used by {@link MediaPlayer2#setDataSource(DataSourceDesc)}
- * to set data source for playback.
- *
- * <p>Users should use {@link Builder} to change {@link DataSourceDesc}.
- *
+ * @see MediaItem2
*/
-@RestrictTo(LIBRARY_GROUP)
public final class DataSourceDesc {
/* No data source has been set yet */
public static final int TYPE_NONE = 0;
@@ -56,8 +49,23 @@
/* data source is type of Uri */
public static final int TYPE_URI = 3;
- // intentionally less than long.MAX_VALUE
- public static final long LONG_MAX = 0x7ffffffffffffffL;
+ // intentionally less than long.MAX_VALUE.
+ // Declare this first to avoid 'illegal forward reference'.
+ private static final long LONG_MAX = 0x7ffffffffffffffL;
+
+ /**
+ * Used when a position is unknown.
+ *
+ * @see #getEndPosition()
+ */
+ public static final long POSITION_UNKNOWN = LONG_MAX;
+
+ /**
+ * Used when the length of file descriptor is unknown.
+ *
+ * @see #getFileDescriptorLength()
+ */
+ public static final long FD_LENGTH_UNKNOWN = LONG_MAX;
private int mType = TYPE_NONE;
@@ -65,7 +73,7 @@
private FileDescriptor mFD;
private long mFDOffset = 0;
- private long mFDLength = LONG_MAX;
+ private long mFDLength = FD_LENGTH_UNKNOWN;
private Uri mUri;
private Map<String, String> mUriHeader;
@@ -74,7 +82,7 @@
private String mMediaId;
private long mStartPositionMs = 0;
- private long mEndPositionMs = LONG_MAX;
+ private long mEndPositionMs = POSITION_UNKNOWN;
private DataSourceDesc() {
}
@@ -83,7 +91,7 @@
* Return the media Id of data source.
* @return the media Id of data source
*/
- public String getMediaId() {
+ public @Nullable String getMediaId() {
return mMediaId;
}
@@ -97,7 +105,7 @@
/**
* Return the position in milliseconds at which the playback will end.
- * -1 means ending at the end of source content.
+ * {@link #POSITION_UNKNOWN} means ending at the end of source content.
* @return the position in milliseconds at which the playback will end
*/
public long getEndPosition() {
@@ -117,7 +125,7 @@
* It's meaningful only when {@code getType} returns {@link #TYPE_CALLBACK}.
* @return the Media2DataSource of this data source
*/
- public Media2DataSource getMedia2DataSource() {
+ public @Nullable Media2DataSource getMedia2DataSource() {
return mMedia2DataSource;
}
@@ -126,7 +134,7 @@
* It's meaningful only when {@code getType} returns {@link #TYPE_FD}.
* @return the FileDescriptor of this data source
*/
- public FileDescriptor getFileDescriptor() {
+ public @Nullable FileDescriptor getFileDescriptor() {
return mFD;
}
@@ -143,7 +151,7 @@
/**
* Return the content length associated with the FileDescriptor of this data source.
* It's meaningful only when {@code getType} returns {@link #TYPE_FD}.
- * -1 means same as the length of source content.
+ * {@link #FD_LENGTH_UNKNOWN} means same as the length of source content.
* @return the content length associated with the FileDescriptor of this data source
*/
public long getFileDescriptorLength() {
@@ -155,7 +163,7 @@
* It's meaningful only when {@code getType} returns {@link #TYPE_URI}.
* @return the Uri of this data source
*/
- public Uri getUri() {
+ public @Nullable Uri getUri() {
return mUri;
}
@@ -164,7 +172,7 @@
* It's meaningful only when {@code getType} returns {@link #TYPE_URI}.
* @return the Uri headers of this data source
*/
- public Map<String, String> getUriHeaders() {
+ public @Nullable Map<String, String> getUriHeaders() {
if (mUriHeader == null) {
return null;
}
@@ -176,7 +184,7 @@
* It's meaningful only when {@code getType} returns {@link #TYPE_URI}.
* @return the Uri cookies of this data source
*/
- public List<HttpCookie> getUriCookies() {
+ public @Nullable List<HttpCookie> getUriCookies() {
if (mUriCookies == null) {
return null;
}
@@ -188,23 +196,12 @@
* It's meaningful only when {@code getType} returns {@link #TYPE_URI}.
* @return the Context used for resolving the Uri of this data source
*/
- public Context getUriContext() {
+ public @Nullable Context getUriContext() {
return mUriContext;
}
/**
* Builder class for {@link DataSourceDesc} objects.
- * <p> Here is an example where <code>Builder</code> is used to define the
- * {@link DataSourceDesc} to be used by a {@link MediaPlayer2} instance:
- *
- * <pre class="prettyprint">
- * DataSourceDesc oldDSD = mediaplayer2.getDataSourceDesc();
- * DataSourceDesc newDSD = new DataSourceDesc.Builder(oldDSD)
- * .setStartPosition(1000)
- * .setEndPosition(15000)
- * .build();
- * mediaplayer2.setDataSourceDesc(newDSD);
- * </pre>
*/
public static class Builder {
private int mType = TYPE_NONE;
@@ -213,7 +210,7 @@
private FileDescriptor mFD;
private long mFDOffset = 0;
- private long mFDLength = LONG_MAX;
+ private long mFDLength = FD_LENGTH_UNKNOWN;
private Uri mUri;
private Map<String, String> mUriHeader;
@@ -222,7 +219,7 @@
private String mMediaId;
private long mStartPositionMs = 0;
- private long mEndPositionMs = LONG_MAX;
+ private long mEndPositionMs = POSITION_UNKNOWN;
/**
* Constructs a new Builder with the defaults.
@@ -235,7 +232,7 @@
* @param dsd the {@link DataSourceDesc} object whose data will be reused
* in the new Builder.
*/
- public Builder(DataSourceDesc dsd) {
+ public Builder(@NonNull DataSourceDesc dsd) {
mType = dsd.mType;
mMedia2DataSource = dsd.mMedia2DataSource;
mFD = dsd.mFD;
@@ -258,7 +255,7 @@
*
* @return a new {@link DataSourceDesc} object
*/
- public DataSourceDesc build() {
+ public @NonNull DataSourceDesc build() {
if (mType != TYPE_CALLBACK
&& mType != TYPE_FD
&& mType != TYPE_URI) {
@@ -293,7 +290,7 @@
* @param mediaId the media Id of this data source
* @return the same Builder instance.
*/
- public Builder setMediaId(String mediaId) {
+ public @NonNull Builder setMediaId(String mediaId) {
mMediaId = mediaId;
return this;
}
@@ -306,7 +303,7 @@
* @return the same Builder instance.
*
*/
- public Builder setStartPosition(long position) {
+ public @NonNull Builder setStartPosition(long position) {
if (position < 0) {
position = 0;
}
@@ -321,9 +318,9 @@
* @param position the end position in milliseconds at which the playback will end
* @return the same Builder instance.
*/
- public Builder setEndPosition(long position) {
+ public @NonNull Builder setEndPosition(long position) {
if (position < 0) {
- position = LONG_MAX;
+ position = POSITION_UNKNOWN;
}
mEndPositionMs = position;
return this;
@@ -336,7 +333,7 @@
* @return the same Builder instance.
* @throws NullPointerException if m2ds is null.
*/
- public Builder setDataSource(@NonNull Media2DataSource m2ds) {
+ public @NonNull Builder setDataSource(@NonNull Media2DataSource m2ds) {
Preconditions.checkNotNull(m2ds);
resetDataSource();
mType = TYPE_CALLBACK;
@@ -353,7 +350,7 @@
* @return the same Builder instance.
* @throws NullPointerException if fd is null.
*/
- public Builder setDataSource(FileDescriptor fd) {
+ public @NonNull Builder setDataSource(@NonNull FileDescriptor fd) {
Preconditions.checkNotNull(fd);
resetDataSource();
mType = TYPE_FD;
@@ -375,13 +372,14 @@
* @return the same Builder instance.
* @throws NullPointerException if fd is null.
*/
- public Builder setDataSource(FileDescriptor fd, long offset, long length) {
+ public @NonNull Builder setDataSource(@NonNull FileDescriptor fd, long offset,
+ long length) {
Preconditions.checkNotNull(fd);
if (offset < 0) {
offset = 0;
}
if (length < 0) {
- length = LONG_MAX;
+ length = FD_LENGTH_UNKNOWN;
}
resetDataSource();
mType = TYPE_FD;
@@ -399,7 +397,7 @@
* @return the same Builder instance.
* @throws NullPointerException if context or uri is null.
*/
- public Builder setDataSource(@NonNull Context context, @NonNull Uri uri) {
+ public @NonNull Builder setDataSource(@NonNull Context context, @NonNull Uri uri) {
Preconditions.checkNotNull(context, "context cannot be null");
Preconditions.checkNotNull(uri, "uri cannot be null");
resetDataSource();
@@ -413,13 +411,7 @@
* Sets the data source as a content Uri.
*
* To provide cookies for the subsequent HTTP requests, you can install your own default
- * cookie handler and use other variants of setDataSource APIs instead. Alternatively, you
- * can use this API to pass the cookies as a list of HttpCookie. If the app has not
- * installed a CookieHandler already, {@link MediaPlayer2} will create a CookieManager
- * and populates its CookieStore with the provided cookies when this data source is passed
- * to {@link MediaPlayer2}. If the app has installed its own handler already, the handler
- * is required to be of CookieManager type such that {@link MediaPlayer2} can update the
- * manager’s CookieStore.
+ * cookie handler and use other variants of setDataSource APIs instead.
*
* <p><strong>Note</strong> that the cross domain redirection is allowed by default,
* but that can be changed with key/value pairs through the headers parameter with
@@ -436,7 +428,7 @@
* @throws IllegalArgumentException if the cookie handler is not of CookieManager type
* when cookies are provided.
*/
- public Builder setDataSource(@NonNull Context context, @NonNull Uri uri,
+ public @NonNull Builder setDataSource(@NonNull Context context, @NonNull Uri uri,
@Nullable Map<String, String> headers, @Nullable List<HttpCookie> cookies) {
Preconditions.checkNotNull(context, "context cannot be null");
Preconditions.checkNotNull(uri);
@@ -467,7 +459,7 @@
mMedia2DataSource = null;
mFD = null;
mFDOffset = 0;
- mFDLength = LONG_MAX;
+ mFDLength = FD_LENGTH_UNKNOWN;
mUri = null;
mUriHeader = null;
mUriCookies = null;
diff --git a/media/src/main/java/androidx/media/MediaController2.java b/media/src/main/java/androidx/media/MediaController2.java
index 05d61ef..1da552d 100644
--- a/media/src/main/java/androidx/media/MediaController2.java
+++ b/media/src/main/java/androidx/media/MediaController2.java
@@ -108,7 +108,6 @@
import android.app.PendingIntent;
import android.content.Context;
import android.media.AudioManager;
-import android.media.session.MediaSessionManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
@@ -127,6 +126,7 @@
import android.util.Log;
import androidx.annotation.GuardedBy;
+import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
@@ -136,14 +136,14 @@
import androidx.media.MediaSession2.ControllerInfo;
import androidx.media.MediaSession2.ErrorCode;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.List;
import java.util.concurrent.Executor;
/**
- * @hide
- * Allows an app to interact with an active {@link MediaSession2} or a
- * {@link MediaSessionService2} in any status. Media buttons and other commands can be sent to
- * the session.
+ * Allows an app to interact with an active {@link MediaSession2} in any status. Media buttons and
+ * other commands can be sent to the session.
* <p>
* When you're done, use {@link #close()} to clean up resources. This also helps session service
* to be destroyed when there's no controller associated with it.
@@ -151,26 +151,32 @@
* When controlling {@link MediaSession2}, the controller will be available immediately after
* the creation.
* <p>
- * When controlling {@link MediaSessionService2}, the {@link MediaController2} would be
- * available only if the session service allows this controller by
- * {@link MediaSession2.SessionCallback#onConnect(MediaSession2, ControllerInfo)} for the service.
- * Wait {@link ControllerCallback#onConnected(MediaController2, SessionCommandGroup2)} or
- * {@link ControllerCallback#onDisconnected(MediaController2)} for the result.
- * <p>
- * A controller can be created through token from {@link MediaSessionManager} if you hold the
- * signature|privileged permission "android.permission.MEDIA_CONTENT_CONTROL" permission or are
- * an enabled notification listener or by getting a {@link SessionToken2} directly the
- * the session owner.
- * <p>
* MediaController2 objects are thread-safe.
* <p>
* @see MediaSession2
- * @see MediaSessionService2
*/
@TargetApi(Build.VERSION_CODES.KITKAT)
-@RestrictTo(LIBRARY_GROUP)
public class MediaController2 implements AutoCloseable {
/**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @IntDef({AudioManager.ADJUST_LOWER, AudioManager.ADJUST_RAISE, AudioManager.ADJUST_SAME,
+ AudioManager.ADJUST_MUTE, AudioManager.ADJUST_UNMUTE, AudioManager.ADJUST_TOGGLE_MUTE})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface VolumeDirection {}
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @IntDef(value = {AudioManager.FLAG_SHOW_UI, AudioManager.FLAG_ALLOW_RINGER_MODES,
+ AudioManager.FLAG_PLAY_SOUND, AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE,
+ AudioManager.FLAG_VIBRATE}, flag = true)
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface VolumeFlags {}
+
+ /**
* Interface for listening to change in activeness of the {@link MediaSession2}. It's
* active if and only if it has set a player.
*/
@@ -297,7 +303,6 @@
* @param item new item
* @see #onBufferingStateChanged(MediaController2, MediaItem2, int)
*/
- // TODO(jaewan): Use this (b/74316764)
public void onCurrentMediaItemChanged(@NonNull MediaController2 controller,
@NonNull MediaItem2 item) { }
@@ -525,7 +530,6 @@
@Override
public void onSessionEvent(String event, Bundle extras) {
- // TODO: Call callbacks on the executor
switch (event) {
case SESSION_EVENT_ON_ALLOWED_COMMANDS_CHANGED: {
SessionCommandGroup2 allowedCommands = SessionCommandGroup2.fromBundle(
@@ -666,7 +670,7 @@
}
private static final String TAG = "MediaController2";
- private static final boolean DEBUG = true; // TODO(jaewan): Change
+ private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
// Note: Using {@code null} doesn't helpful here because MediaBrowserServiceCompat always wraps
// the rootHints so it becomes non-null.
@@ -1090,7 +1094,7 @@
* @param flags flags from {@link AudioManager} to include with the volume request for local
* playback
*/
- public void setVolumeTo(int value, int flags) {
+ public void setVolumeTo(int value, @VolumeFlags int flags) {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
@@ -1107,6 +1111,7 @@
* Adjust the volume of the output this session is playing on. The direction
* must be one of {@link AudioManager#ADJUST_LOWER},
* {@link AudioManager#ADJUST_RAISE}, or {@link AudioManager#ADJUST_SAME}.
+ * <p>
* The command will be ignored if the session does not support
* {@link VolumeProviderCompat#VOLUME_CONTROL_RELATIVE} or
* {@link VolumeProviderCompat#VOLUME_CONTROL_ABSOLUTE}.
@@ -1122,7 +1127,7 @@
* @param flags flags from {@link AudioManager} to include with the volume request for local
* playback
*/
- public void adjustVolume(int direction, int flags) {
+ public void adjustVolume(@VolumeDirection int direction, @VolumeFlags int flags) {
synchronized (mLock) {
if (!mConnected) {
Log.w(TAG, "Session isn't active", new IllegalStateException());
@@ -1274,7 +1279,6 @@
*/
public @Nullable PlaybackInfo getPlaybackInfo() {
synchronized (mLock) {
- // TODO: update mPlaybackInfo via MediaControllerCompat.Callback.onAudioInfoChanged().
return mPlaybackInfo;
}
}
@@ -1333,7 +1337,9 @@
* implementation. Use media items returned here for other playlist agent APIs such as
* {@link MediaPlaylistAgent#skipToPlaylistItem(MediaItem2)}.
*
- * @return playlist. Can be {@code null} if the controller doesn't have enough permission.
+ * @return playlist. Can be {@code null} if the playlist hasn't set nor controller doesn't have
+ * enough permission.
+ * @see SessionCommand2#COMMAND_CODE_PLAYLIST_GET_LIST
*/
public @Nullable List<MediaItem2> getPlaylist() {
synchronized (mLock) {
@@ -1389,7 +1395,8 @@
/**
* Adds the media item to the playlist at position index. Index equals or greater than
- * the current playlist size will add the item at the end of the playlist.
+ * the current playlist size (e.g. {@link Integer#MAX_VALUE}) will add the item at the end of
+ * the playlist.
* <p>
* This will not change the currently playing media item.
* If index is less than or equal to the current index of the playlist,
@@ -1501,7 +1508,6 @@
* @see MediaPlaylistAgent#REPEAT_MODE_GROUP
*/
public void setRepeatMode(@RepeatMode int repeatMode) {
- // TODO: check permission
Bundle args = new Bundle();
args.putInt(ARGUMENT_REPEAT_MODE, repeatMode);
sendCommand(COMMAND_CODE_PLAYLIST_SET_REPEAT_MODE, args);
@@ -1530,7 +1536,6 @@
* @see MediaPlaylistAgent#SHUFFLE_MODE_GROUP
*/
public void setShuffleMode(@ShuffleMode int shuffleMode) {
- // TODO: check permission
Bundle args = new Bundle();
args.putInt(ARGUMENT_SHUFFLE_MODE, shuffleMode);
sendCommand(COMMAND_CODE_PLAYLIST_SET_SHUFFLE_MODE, args);
@@ -1570,7 +1575,6 @@
// Should be used without a lock to prevent potential deadlock.
void onConnectedNotLocked(Bundle data) {
- // TODO: Getting mPlaybackInfo via MediaControllerCompat.Callback.onAudioInfoChanged()
// is enough or should we pass it while connecting?
final SessionCommandGroup2 allowedCommands = SessionCommandGroup2.fromBundle(
data.getBundle(ARGUMENT_ALLOWED_COMMANDS));
@@ -1580,13 +1584,14 @@
ARGUMENT_PLAYBACK_STATE_COMPAT);
final int repeatMode = data.getInt(ARGUMENT_REPEAT_MODE);
final int shuffleMode = data.getInt(ARGUMENT_SHUFFLE_MODE);
- // TODO: Set mMediaMetadataCompat from the data.
final List<MediaItem2> playlist = MediaUtils2.fromMediaItem2ParcelableArray(
data.getParcelableArray(ARGUMENT_PLAYLIST));
final MediaItem2 currentMediaItem = MediaItem2.fromBundle(
data.getBundle(ARGUMENT_MEDIA_ITEM));
final PlaybackInfo playbackInfo =
PlaybackInfo.fromBundle(data.getBundle(ARGUMENT_PLAYBACK_INFO));
+ final MediaMetadata2 metadata = MediaMetadata2.fromBundle(
+ data.getBundle(ARGUMENT_PLAYLIST_METADATA));
if (DEBUG) {
Log.d(TAG, "onConnectedNotLocked sessionCompatToken=" + mToken.getSessionCompatToken()
+ ", allowedCommands=" + allowedCommands);
@@ -1611,10 +1616,10 @@
mShuffleMode = shuffleMode;
mPlaylist = playlist;
mCurrentMediaItem = currentMediaItem;
+ mPlaylistMetadata = metadata;
mConnected = true;
mPlaybackInfo = playbackInfo;
}
- // TODO(jaewan): Keep commands to prevents illegal API calls.
mCallbackExecutor.execute(new Runnable() {
@Override
public void run() {
@@ -1634,8 +1639,6 @@
}
private void initialize() {
- // TODO(jaewan): More sanity checks.
- // TODO: Check the connection between 1.0 and 2.0 APIs
if (mToken.getType() == SessionToken2.TYPE_SESSION) {
synchronized (mLock) {
mBrowserCompat = null;
diff --git a/media/src/main/java/androidx/media/MediaItem2.java b/media/src/main/java/androidx/media/MediaItem2.java
index 20fbf06..b8c44c1 100644
--- a/media/src/main/java/androidx/media/MediaItem2.java
+++ b/media/src/main/java/androidx/media/MediaItem2.java
@@ -31,7 +31,6 @@
import java.util.UUID;
/**
- * @hide
* A class with information on a single media item with the metadata information.
* Media item are application dependent so we cannot guarantee that they contain the right values.
* <p>
@@ -39,7 +38,6 @@
* <p>
* This object isn't a thread safe.
*/
-@RestrictTo(LIBRARY_GROUP)
public class MediaItem2 {
/** @hide */
@RestrictTo(LIBRARY_GROUP)
@@ -298,7 +296,6 @@
String id = (mMetadata != null)
? mMetadata.getString(MediaMetadata2.METADATA_KEY_MEDIA_ID) : null;
if (id == null) {
- // TODO(jaewan): Double check if its sufficient (e.g. Use UUID instead?)
id = (mMediaId != null) ? mMediaId : toString();
}
return new MediaItem2(id, mDataSourceDesc, mMetadata, mFlags);
diff --git a/media/src/main/java/androidx/media/MediaMetadata2.java b/media/src/main/java/androidx/media/MediaMetadata2.java
index f622bfc..0cfd237 100644
--- a/media/src/main/java/androidx/media/MediaMetadata2.java
+++ b/media/src/main/java/androidx/media/MediaMetadata2.java
@@ -34,14 +34,12 @@
import java.util.Set;
/**
- * @hide
* Contains metadata about an item, such as the title, artist, etc.
*/
// New version of MediaMetadata with following changes
// - Don't implement Parcelable for updatable support.
// - Also support MediaDescription features. MediaDescription is deprecated instead because
// it was insufficient for controller to display media contents.
-@RestrictTo(LIBRARY_GROUP)
public final class MediaMetadata2 {
private static final String TAG = "MediaMetadata2";
@@ -339,8 +337,7 @@
* service providing the content. If used, this should be a persistent
* unique key for the underlying content. It may be used with
* {@link MediaController2#playFromMediaId(String, Bundle)}
- * to initiate playback when provided by a {@link MediaBrowser2} connected to
- * the same app.
+ * to initiate playback.
*
* @see Builder#putText(String, CharSequence)
* @see Builder#putString(String, String)
@@ -353,7 +350,7 @@
* The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
* information about the Uri of the content. This value is specific to the service providing the
* content. It may be used with {@link MediaController2#playFromUri(Uri, Bundle)}
- * to initiate playback when provided by a {@link MediaBrowser2} connected to the same app.
+ * to initiate playback.
*
* @see Builder#putText(String, CharSequence)
* @see Builder#putString(String, String)
@@ -714,7 +711,6 @@
if (key == null) {
throw new IllegalArgumentException("key shouldn't be null");
}
- // TODO(jaewan): Add backward compatibility
Rating2 rating = null;
try {
rating = Rating2.fromBundle(mBundle.getBundle(key));
@@ -887,7 +883,6 @@
* <li>{@link #METADATA_KEY_DISPLAY_ICON_URI}</li>
* <li>{@link #METADATA_KEY_MEDIA_ID}</li>
* <li>{@link #METADATA_KEY_MEDIA_URI}</li>
- * <li>{@link #METADATA_KEY_RADIO_PROGRAM_NAME}</li>
* </ul>
*
* @param key The key for referencing this value
@@ -932,7 +927,6 @@
* <li>{@link #METADATA_KEY_DISPLAY_ICON_URI}</li>
* <li>{@link #METADATA_KEY_MEDIA_ID}</li>
* <li>{@link #METADATA_KEY_MEDIA_URI}</li>
- * <li>{@link #METADATA_KEY_RADIO_PROGRAM_NAME}</li>
* </ul>
*
* @param key The key for referencing this value
diff --git a/media/src/main/java/androidx/media/MediaPlayer2.java b/media/src/main/java/androidx/media/MediaPlayer2.java
index 1864d72..3802b9f 100644
--- a/media/src/main/java/androidx/media/MediaPlayer2.java
+++ b/media/src/main/java/androidx/media/MediaPlayer2.java
@@ -28,6 +28,7 @@
import android.media.MediaTimestamp;
import android.media.PlaybackParams;
import android.media.ResourceBusyException;
+import android.media.SubtitleData;
import android.media.SyncParams;
import android.media.TimedMetaData;
import android.media.UnsupportedSchemeException;
@@ -47,7 +48,6 @@
import java.util.UUID;
import java.util.concurrent.Executor;
-
/**
* @hide
* MediaPlayer2 class can be used to control playback
@@ -832,10 +832,7 @@
*
* Additional vendor-specific fields may also be present in
* the return value.
- * @hide
- * TODO: This method is not ready for public. Currently returns metrics data in MediaPlayer1.
*/
- @RestrictTo(LIBRARY_GROUP)
public abstract PersistableBundle getMetrics();
/**
@@ -1229,13 +1226,26 @@
@CallStatus int status) { }
/**
- * Called to indicate media clock has changed.
+ * Called when a discontinuity in the normal progression of the media time is detected.
+ * The "normal progression" of media time is defined as the expected increase of the
+ * playback position when playing media, relative to the playback speed (for instance every
+ * second, media time increases by two seconds when playing at 2x).<br>
+ * Discontinuities are encountered in the following cases:
+ * <ul>
+ * <li>when the player is starved for data and cannot play anymore</li>
+ * <li>when the player encounters a playback error</li>
+ * <li>when the a seek operation starts, and when it's completed</li>
+ * <li>when the playback speed changes</li>
+ * <li>when the playback state changes</li>
+ * <li>when the player is reset</li>
+ * </ul>
*
* @param mp the MediaPlayer2 the media time pertains to.
* @param dsd the DataSourceDesc of this data source
- * @param timestamp the new media clock.
+ * @param timestamp the timestamp that correlates media time, system time and clock rate,
+ * or {@link MediaTimestamp#TIMESTAMP_UNKNOWN} in an error case.
*/
- public void onMediaTimeChanged(
+ public void onMediaTimeDiscontinuity(
MediaPlayer2 mp, DataSourceDesc dsd, MediaTimestamp timestamp) { }
/**
@@ -1247,12 +1257,14 @@
*/
public void onCommandLabelReached(MediaPlayer2 mp, @NonNull Object label) { }
- /* TODO : uncomment below once API is available in supportlib.
+ /**
* Called when when a player subtitle track has new subtitle data available.
* @param mp the player that reports the new subtitle data
+ * @param dsd the DataSourceDesc of this data source
* @param data the subtitle data
*/
- // public void onSubtitleData(MediaPlayer2 mp, @NonNull SubtitleData data) { }
+ public void onSubtitleData(
+ MediaPlayer2 mp, DataSourceDesc dsd, @NonNull SubtitleData data) { }
}
/**
@@ -2008,4 +2020,100 @@
super(detailMessage);
}
}
+
+ /**
+ * Definitions for the metrics that are reported via the {@link #getMetrics} call.
+ */
+ public static final class MetricsConstants {
+ private MetricsConstants() {}
+
+ /**
+ * Key to extract the MIME type of the video track
+ * from the {@link MediaPlayer2#getMetrics} return value.
+ * The value is a String.
+ */
+ public static final String MIME_TYPE_VIDEO = "android.media.mediaplayer.video.mime";
+
+ /**
+ * Key to extract the codec being used to decode the video track
+ * from the {@link MediaPlayer2#getMetrics} return value.
+ * The value is a String.
+ */
+ public static final String CODEC_VIDEO = "android.media.mediaplayer.video.codec";
+
+ /**
+ * Key to extract the width (in pixels) of the video track
+ * from the {@link MediaPlayer2#getMetrics} return value.
+ * The value is an integer.
+ */
+ public static final String WIDTH = "android.media.mediaplayer.width";
+
+ /**
+ * Key to extract the height (in pixels) of the video track
+ * from the {@link MediaPlayer2#getMetrics} return value.
+ * The value is an integer.
+ */
+ public static final String HEIGHT = "android.media.mediaplayer.height";
+
+ /**
+ * Key to extract the count of video frames played
+ * from the {@link MediaPlayer2#getMetrics} return value.
+ * The value is an integer.
+ */
+ public static final String FRAMES = "android.media.mediaplayer.frames";
+
+ /**
+ * Key to extract the count of video frames dropped
+ * from the {@link MediaPlayer2#getMetrics} return value.
+ * The value is an integer.
+ */
+ public static final String FRAMES_DROPPED = "android.media.mediaplayer.dropped";
+
+ /**
+ * Key to extract the MIME type of the audio track
+ * from the {@link MediaPlayer2#getMetrics} return value.
+ * The value is a String.
+ */
+ public static final String MIME_TYPE_AUDIO = "android.media.mediaplayer.audio.mime";
+
+ /**
+ * Key to extract the codec being used to decode the audio track
+ * from the {@link MediaPlayer2#getMetrics} return value.
+ * The value is a String.
+ */
+ public static final String CODEC_AUDIO = "android.media.mediaplayer.audio.codec";
+
+ /**
+ * Key to extract the duration (in milliseconds) of the
+ * media being played
+ * from the {@link MediaPlayer2#getMetrics} return value.
+ * The value is a long.
+ */
+ public static final String DURATION = "android.media.mediaplayer.durationMs";
+
+ /**
+ * Key to extract the playing time (in milliseconds) of the
+ * media being played
+ * from the {@link MediaPlayer2#getMetrics} return value.
+ * The value is a long.
+ */
+ public static final String PLAYING = "android.media.mediaplayer.playingMs";
+
+ /**
+ * Key to extract the count of errors encountered while
+ * playing the media
+ * from the {@link MediaPlayer2#getMetrics} return value.
+ * The value is an integer.
+ */
+ public static final String ERRORS = "android.media.mediaplayer.err";
+
+ /**
+ * Key to extract an (optional) error code detected while
+ * playing the media
+ * from the {@link MediaPlayer2#getMetrics} return value.
+ * The value is an integer.
+ */
+ public static final String ERROR_CODE = "android.media.mediaplayer.errcode";
+
+ }
}
diff --git a/media/src/main/java/androidx/media/MediaPlayer2Impl.java b/media/src/main/java/androidx/media/MediaPlayer2Impl.java
index a4bb01e..50c490f 100644
--- a/media/src/main/java/androidx/media/MediaPlayer2Impl.java
+++ b/media/src/main/java/androidx/media/MediaPlayer2Impl.java
@@ -28,6 +28,7 @@
import android.media.MediaTimestamp;
import android.media.PlaybackParams;
import android.media.ResourceBusyException;
+import android.media.SubtitleData;
import android.media.SyncParams;
import android.media.TimedMetaData;
import android.media.UnsupportedSchemeException;
@@ -75,13 +76,53 @@
private static final int NEXT_SOURCE_STATE_PREPARING = 1;
private static final int NEXT_SOURCE_STATE_PREPARED = 2;
- // TODO: This class has too many locks. Use one single lock to protect internal variables.
- private MediaPlayer mPlayer;
- @PlayerState
- private int mPlayerState;
- @BuffState
- private int mBufferingState;
- private AudioAttributesCompat mAudioAttributes;
+ private static ArrayMap<Integer, Integer> sInfoEventMap;
+ private static ArrayMap<Integer, Integer> sErrorEventMap;
+ private static ArrayMap<Integer, Integer> sPrepareDrmStatusMap;
+
+ static {
+ sInfoEventMap = new ArrayMap<>();
+ sInfoEventMap.put(MediaPlayer.MEDIA_INFO_UNKNOWN, MEDIA_INFO_UNKNOWN);
+ sInfoEventMap.put(2 /*MediaPlayer.MEDIA_INFO_STARTED_AS_NEXT*/, MEDIA_INFO_STARTED_AS_NEXT);
+ sInfoEventMap.put(
+ MediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START, MEDIA_INFO_VIDEO_RENDERING_START);
+ sInfoEventMap.put(
+ MediaPlayer.MEDIA_INFO_VIDEO_TRACK_LAGGING, MEDIA_INFO_VIDEO_TRACK_LAGGING);
+ sInfoEventMap.put(MediaPlayer.MEDIA_INFO_BUFFERING_START, MEDIA_INFO_BUFFERING_START);
+ sInfoEventMap.put(MediaPlayer.MEDIA_INFO_BUFFERING_END, MEDIA_INFO_BUFFERING_END);
+ sInfoEventMap.put(MediaPlayer.MEDIA_INFO_BAD_INTERLEAVING, MEDIA_INFO_BAD_INTERLEAVING);
+ sInfoEventMap.put(MediaPlayer.MEDIA_INFO_NOT_SEEKABLE, MEDIA_INFO_NOT_SEEKABLE);
+ sInfoEventMap.put(MediaPlayer.MEDIA_INFO_METADATA_UPDATE, MEDIA_INFO_METADATA_UPDATE);
+ sInfoEventMap.put(MediaPlayer.MEDIA_INFO_AUDIO_NOT_PLAYING, MEDIA_INFO_AUDIO_NOT_PLAYING);
+ sInfoEventMap.put(MediaPlayer.MEDIA_INFO_VIDEO_NOT_PLAYING, MEDIA_INFO_VIDEO_NOT_PLAYING);
+ sInfoEventMap.put(
+ MediaPlayer.MEDIA_INFO_UNSUPPORTED_SUBTITLE, MEDIA_INFO_UNSUPPORTED_SUBTITLE);
+ sInfoEventMap.put(MediaPlayer.MEDIA_INFO_SUBTITLE_TIMED_OUT, MEDIA_INFO_SUBTITLE_TIMED_OUT);
+
+ sErrorEventMap = new ArrayMap<>();
+ sErrorEventMap.put(MediaPlayer.MEDIA_ERROR_UNKNOWN, MEDIA_ERROR_UNKNOWN);
+ sErrorEventMap.put(
+ MediaPlayer.MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK,
+ MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK);
+ sErrorEventMap.put(MediaPlayer.MEDIA_ERROR_IO, MEDIA_ERROR_IO);
+ sErrorEventMap.put(MediaPlayer.MEDIA_ERROR_MALFORMED, MEDIA_ERROR_MALFORMED);
+ sErrorEventMap.put(MediaPlayer.MEDIA_ERROR_UNSUPPORTED, MEDIA_ERROR_UNSUPPORTED);
+ sErrorEventMap.put(MediaPlayer.MEDIA_ERROR_TIMED_OUT, MEDIA_ERROR_TIMED_OUT);
+
+ sPrepareDrmStatusMap.put(
+ MediaPlayer.PREPARE_DRM_STATUS_SUCCESS, PREPARE_DRM_STATUS_SUCCESS);
+ sPrepareDrmStatusMap.put(
+ MediaPlayer.PREPARE_DRM_STATUS_PROVISIONING_NETWORK_ERROR,
+ PREPARE_DRM_STATUS_PROVISIONING_NETWORK_ERROR);
+ sPrepareDrmStatusMap.put(
+ MediaPlayer.PREPARE_DRM_STATUS_PROVISIONING_SERVER_ERROR,
+ PREPARE_DRM_STATUS_PROVISIONING_SERVER_ERROR);
+ sPrepareDrmStatusMap.put(
+ MediaPlayer.PREPARE_DRM_STATUS_PROVISIONING_SERVER_ERROR,
+ PREPARE_DRM_STATUS_PROVISIONING_SERVER_ERROR);
+ }
+
+ private MediaPlayer mPlayer; // MediaPlayer is thread-safe.
private final Object mSrcLock = new Object();
//--- guarded by |mSrcLock| start
@@ -106,18 +147,18 @@
@GuardedBy("mTaskLock")
private Task mCurrentTask;
- private final Object mEventCbLock = new Object();
- @GuardedBy("mEventCbLock")
+ private final Object mLock = new Object();
+ //--- guarded by |mLock| start
+ @PlayerState private int mPlayerState;
+ @BuffState private int mBufferingState;
+ private AudioAttributesCompat mAudioAttributes;
private ArrayList<Pair<Executor, MediaPlayer2EventCallback>> mMp2EventCallbackRecords =
new ArrayList<>();
- @GuardedBy("mEventCbLock")
private ArrayMap<PlayerEventCallback, Executor> mPlayerEventCallbackMap =
new ArrayMap<>();
-
- private final Object mDrmEventCbLock = new Object();
- @GuardedBy("mDrmEventCbLock")
private ArrayList<Pair<Executor, DrmEventCallback>> mDrmEventCallbackRecords =
new ArrayList<>();
+ //--- guarded by |mLock| end
/**
* Default constructor.
@@ -130,6 +171,9 @@
mHandlerThread.start();
Looper looper = mHandlerThread.getLooper();
mTaskHandler = new Handler(looper);
+
+ // TODO: To make sure MediaPlayer1 listeners work, the caller thread should have a looper.
+ // Fix the framework or document this behavior.
mPlayer = new MediaPlayer();
mPlayerState = PLAYER_STATE_IDLE;
mBufferingState = BUFFERING_STATE_UNKNOWN;
@@ -273,7 +317,9 @@
@Override
public @PlayerState int getPlayerState() {
- return mPlayerState;
+ synchronized (mLock) {
+ return mPlayerState;
+ }
}
/**
@@ -283,7 +329,9 @@
*/
@Override
public @BuffState int getBufferingState() {
- return mBufferingState;
+ synchronized (mLock) {
+ return mBufferingState;
+ }
}
/**
@@ -299,15 +347,21 @@
addTask(new Task(CALL_COMPLETED_SET_AUDIO_ATTRIBUTES, false) {
@Override
void process() {
- mAudioAttributes = attributes;
- mPlayer.setAudioAttributes((AudioAttributes) mAudioAttributes.unwrap());
+ AudioAttributes attr;
+ synchronized (mLock) {
+ mAudioAttributes = attributes;
+ attr = (AudioAttributes) mAudioAttributes.unwrap();
+ }
+ mPlayer.setAudioAttributes(attr);
}
});
}
@Override
public @NonNull AudioAttributesCompat getAudioAttributes() {
- return mAudioAttributes;
+ synchronized (mLock) {
+ return mAudioAttributes;
+ }
}
/**
@@ -523,7 +577,7 @@
throw new IllegalArgumentException(
"Illegal null Executor for the PlayerEventCallback");
}
- synchronized (mEventCbLock) {
+ synchronized (mLock) {
mPlayerEventCallbackMap.put(cb, e);
}
}
@@ -537,7 +591,7 @@
if (cb == null) {
throw new IllegalArgumentException("Illegal null PlayerEventCallback");
}
- synchronized (mEventCbLock) {
+ synchronized (mLock) {
mPlayerEventCallbackMap.remove(cb);
}
}
@@ -547,18 +601,12 @@
addTask(new Task(CALL_COMPLETED_NOTIFY_WHEN_COMMAND_LABEL_REACHED, false) {
@Override
void process() {
- synchronized (mEventCbLock) {
- for (final Pair<Executor, MediaPlayer2EventCallback> cb
- : mMp2EventCallbackRecords) {
- cb.first.execute(new Runnable() {
- @Override
- public void run() {
- cb.second.onCommandLabelReached(
- MediaPlayer2Impl.this, label);
- }
- });
+ notifyMediaPlayer2Event(new Mp2EventNotifier() {
+ @Override
+ public void notify(MediaPlayer2EventCallback cb) {
+ cb.onCommandLabelReached(MediaPlayer2Impl.this, label);
}
- }
+ });
}
});
}
@@ -840,9 +888,20 @@
@Override
public void reset() {
mPlayer.reset();
+
+ mBufferedPercentageCurrent.set(0);
+ mBufferedPercentageNext.set(0);
+ mVolume = 1.0f;
+
+ synchronized (mLock) {
+ mAudioAttributes = null;
+ mMp2EventCallbackRecords.clear();
+ mPlayerEventCallbackMap.clear();
+ mDrmEventCallbackRecords.clear();
+ }
setPlayerState(PLAYER_STATE_IDLE);
setBufferingState(BUFFERING_STATE_UNKNOWN);
- /* FIXME: reset other internal variables. */
+ setUpListeners();
}
/**
@@ -1169,7 +1228,7 @@
throw new IllegalArgumentException(
"Illegal null Executor for the MediaPlayer2EventCallback");
}
- synchronized (mEventCbLock) {
+ synchronized (mLock) {
mMp2EventCallbackRecords.add(new Pair(executor, eventCallback));
}
}
@@ -1179,7 +1238,7 @@
*/
@Override
public void clearMediaPlayer2EventCallback() {
- synchronized (mEventCbLock) {
+ synchronized (mLock) {
mMp2EventCallbackRecords.clear();
}
}
@@ -1199,8 +1258,7 @@
mPlayer.setOnDrmConfigHelper(new MediaPlayer.OnDrmConfigHelper() {
@Override
public void onDrmConfig(MediaPlayer mp) {
- /** FIXME: pass the right DSD. */
- listener.onDrmConfig(MediaPlayer2Impl.this, null);
+ listener.onDrmConfig(MediaPlayer2Impl.this, mCurrentDSD);
}
});
}
@@ -1222,7 +1280,7 @@
throw new IllegalArgumentException(
"Illegal null Executor for the MediaPlayer2EventCallback");
}
- synchronized (mDrmEventCbLock) {
+ synchronized (mLock) {
mDrmEventCallbackRecords.add(new Pair(executor, eventCallback));
}
}
@@ -1232,7 +1290,7 @@
*/
@Override
public void clearDrmEventCallback() {
- synchronized (mDrmEventCbLock) {
+ synchronized (mLock) {
mDrmEventCallbackRecords.clear();
}
}
@@ -1472,60 +1530,105 @@
private void setPlaybackParamsInternal(final PlaybackParams params) {
PlaybackParams current = mPlayer.getPlaybackParams();
- if (Math.abs(current.getSpeed() - params.getSpeed()) > 0.0001f) {
- synchronized (mEventCbLock) {
- final int callbackCount = mPlayerEventCallbackMap.size();
- for (int i = 0; i < callbackCount; i++) {
- final Executor executor = mPlayerEventCallbackMap.valueAt(i);
- final PlayerEventCallback cb = mPlayerEventCallbackMap.keyAt(i);
- executor.execute(new Runnable() {
- @Override
- public void run() {
- cb.onPlaybackSpeedChanged(MediaPlayer2Impl.this, params.getSpeed());
- }
- });
- }
- }
- }
mPlayer.setPlaybackParams(params);
+ if (current.getSpeed() != params.getSpeed()) {
+ notifyPlayerEvent(new PlayerEventNotifier() {
+ @Override
+ public void notify(PlayerEventCallback cb) {
+ cb.onPlaybackSpeedChanged(MediaPlayer2Impl.this, params.getSpeed());
+ }
+ });
+ }
}
private void setPlayerState(@PlayerState final int state) {
- if (mPlayerState != state) {
- synchronized (mEventCbLock) {
- final int callbackCount = mPlayerEventCallbackMap.size();
- for (int i = 0; i < callbackCount; i++) {
- final Executor executor = mPlayerEventCallbackMap.valueAt(i);
- final PlayerEventCallback cb = mPlayerEventCallbackMap.keyAt(i);
- executor.execute(new Runnable() {
- @Override
- public void run() {
- cb.onPlayerStateChanged(MediaPlayer2Impl.this, state);
- }
- });
- }
+ synchronized (mLock) {
+ if (mPlayerState == state) {
+ return;
}
mPlayerState = state;
}
+ notifyPlayerEvent(new PlayerEventNotifier() {
+ @Override
+ public void notify(PlayerEventCallback cb) {
+ cb.onPlayerStateChanged(MediaPlayer2Impl.this, state);
+ }
+ });
}
private void setBufferingState(@BuffState final int state) {
- if (mBufferingState != state) {
- synchronized (mEventCbLock) {
- final int callbackCount = mPlayerEventCallbackMap.size();
- for (int i = 0; i < callbackCount; i++) {
- final Executor executor = mPlayerEventCallbackMap.valueAt(i);
- final PlayerEventCallback cb = mPlayerEventCallbackMap.keyAt(i);
- executor.execute(new Runnable() {
- @Override
- public void run() {
- cb.onBufferingStateChanged(MediaPlayer2Impl.this, mCurrentDSD, state);
- }
- });
- }
+ synchronized (mLock) {
+ if (mBufferingState == state) {
+ return;
}
mBufferingState = state;
}
+ notifyPlayerEvent(new PlayerEventNotifier() {
+ @Override
+ public void notify(PlayerEventCallback cb) {
+ cb.onBufferingStateChanged(MediaPlayer2Impl.this, mCurrentDSD, state);
+ }
+ });
+ }
+
+ private void notifyMediaPlayer2Event(final Mp2EventNotifier notifier) {
+ List<Pair<Executor, MediaPlayer2EventCallback>> records;
+ synchronized (mLock) {
+ records = new ArrayList<>(mMp2EventCallbackRecords);
+ }
+ for (final Pair<Executor, MediaPlayer2EventCallback> record : records) {
+ record.first.execute(new Runnable() {
+ @Override
+ public void run() {
+ notifier.notify(record.second);
+ }
+ });
+ }
+ }
+
+ private void notifyPlayerEvent(final PlayerEventNotifier notifier) {
+ ArrayMap<PlayerEventCallback, Executor> map;
+ synchronized (mLock) {
+ map = new ArrayMap<>(mPlayerEventCallbackMap);
+ }
+ final int callbackCount = map.size();
+ for (int i = 0; i < callbackCount; i++) {
+ final Executor executor = map.valueAt(i);
+ final PlayerEventCallback cb = map.keyAt(i);
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ notifier.notify(cb);
+ }
+ });
+ }
+ }
+
+ private void notifyDrmEvent(final DrmEventNotifier notifier) {
+ List<Pair<Executor, DrmEventCallback>> records;
+ synchronized (mLock) {
+ records = new ArrayList<>(mDrmEventCallbackRecords);
+ }
+ for (final Pair<Executor, DrmEventCallback> record : records) {
+ record.first.execute(new Runnable() {
+ @Override
+ public void run() {
+ notifier.notify(record.second);
+ }
+ });
+ }
+ }
+
+ private interface Mp2EventNotifier {
+ void notify(MediaPlayer2EventCallback callback);
+ }
+
+ private interface PlayerEventNotifier {
+ void notify(PlayerEventCallback callback);
+ }
+
+ private interface DrmEventNotifier {
+ void notify(DrmEventCallback callback);
}
private void setUpListeners() {
@@ -1534,29 +1637,18 @@
public void onPrepared(MediaPlayer mp) {
setPlayerState(PLAYER_STATE_PAUSED);
setBufferingState(BUFFERING_STATE_BUFFERING_AND_PLAYABLE);
- synchronized (mEventCbLock) {
- for (final Pair<Executor, MediaPlayer2EventCallback> cb :
- mMp2EventCallbackRecords) {
- cb.first.execute(new Runnable() {
- @Override
- public void run() {
- cb.second.onInfo(MediaPlayer2Impl.this, mCurrentDSD,
- MEDIA_INFO_PREPARED, 0);
- }
- });
+ notifyMediaPlayer2Event(new Mp2EventNotifier() {
+ @Override
+ public void notify(MediaPlayer2EventCallback callback) {
+ callback.onInfo(MediaPlayer2Impl.this, mCurrentDSD, MEDIA_INFO_PREPARED, 0);
}
- final int callbackCount = mPlayerEventCallbackMap.size();
- for (int i = 0; i < callbackCount; i++) {
- final Executor executor = mPlayerEventCallbackMap.valueAt(i);
- final PlayerEventCallback cb = mPlayerEventCallbackMap.keyAt(i);
- executor.execute(new Runnable() {
- @Override
- public void run() {
- cb.onMediaPrepared(MediaPlayer2Impl.this, mCurrentDSD);
- }
- });
+ });
+ notifyPlayerEvent(new PlayerEventNotifier() {
+ @Override
+ public void notify(PlayerEventCallback cb) {
+ cb.onMediaPrepared(MediaPlayer2Impl.this, mCurrentDSD);
}
- }
+ });
synchronized (mTaskLock) {
if (mCurrentTask != null
&& mCurrentTask.mMediaCallType == CALL_COMPLETED_PREPARE
@@ -1572,18 +1664,12 @@
mPlayer.setOnVideoSizeChangedListener(new MediaPlayer.OnVideoSizeChangedListener() {
@Override
public void onVideoSizeChanged(MediaPlayer mp, final int width, final int height) {
- synchronized (mEventCbLock) {
- for (final Pair<Executor, MediaPlayer2EventCallback> cb :
- mMp2EventCallbackRecords) {
- cb.first.execute(new Runnable() {
- @Override
- public void run() {
- cb.second.onVideoSizeChanged(MediaPlayer2Impl.this, mCurrentDSD,
- width, height);
- }
- });
+ notifyMediaPlayer2Event(new Mp2EventNotifier() {
+ @Override
+ public void notify(MediaPlayer2EventCallback cb) {
+ cb.onVideoSizeChanged(MediaPlayer2Impl.this, mCurrentDSD, width, height);
}
- }
+ });
}
});
mPlayer.setOnInfoListener(new MediaPlayer.OnInfoListener() {
@@ -1591,18 +1677,13 @@
public boolean onInfo(MediaPlayer mp, int what, int extra) {
switch (what) {
case MediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START:
- synchronized (mEventCbLock) {
- for (final Pair<Executor, MediaPlayer2EventCallback> cb :
- mMp2EventCallbackRecords) {
- cb.first.execute(new Runnable() {
- @Override
- public void run() {
- cb.second.onInfo(MediaPlayer2Impl.this, mCurrentDSD,
- MEDIA_INFO_VIDEO_RENDERING_START, 0);
- }
- });
+ notifyMediaPlayer2Event(new Mp2EventNotifier() {
+ @Override
+ public void notify(MediaPlayer2EventCallback cb) {
+ cb.onInfo(MediaPlayer2Impl.this, mCurrentDSD,
+ MEDIA_INFO_VIDEO_RENDERING_START, 0);
}
- }
+ });
break;
case MediaPlayer.MEDIA_INFO_BUFFERING_START:
setBufferingState(BUFFERING_STATE_BUFFERING_AND_STARVED);
@@ -1618,18 +1699,13 @@
@Override
public void onCompletion(MediaPlayer mp) {
setPlayerState(PLAYER_STATE_PAUSED);
- synchronized (mEventCbLock) {
- for (final Pair<Executor, MediaPlayer2EventCallback> cb :
- mMp2EventCallbackRecords) {
- cb.first.execute(new Runnable() {
- @Override
- public void run() {
- cb.second.onInfo(MediaPlayer2Impl.this, mCurrentDSD,
- MEDIA_INFO_PLAYBACK_COMPLETE, 0);
- }
- });
+ notifyMediaPlayer2Event(new Mp2EventNotifier() {
+ @Override
+ public void notify(MediaPlayer2EventCallback cb) {
+ cb.onInfo(MediaPlayer2Impl.this, mCurrentDSD, MEDIA_INFO_PLAYBACK_COMPLETE,
+ 0);
}
- }
+ });
}
});
mPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
@@ -1637,19 +1713,13 @@
public boolean onError(MediaPlayer mp, final int what, final int extra) {
setPlayerState(PLAYER_STATE_ERROR);
setBufferingState(BUFFERING_STATE_UNKNOWN);
- synchronized (mEventCbLock) {
- for (final Pair<Executor, MediaPlayer2EventCallback> cb :
- mMp2EventCallbackRecords) {
- cb.first.execute(new Runnable() {
- @Override
- public void run() {
- // TODO: translate what value to MP2's definition.
- cb.second.onError(MediaPlayer2Impl.this, mCurrentDSD,
- what, extra);
- }
- });
+ notifyMediaPlayer2Event(new Mp2EventNotifier() {
+ @Override
+ public void notify(MediaPlayer2EventCallback cb) {
+ int w = sErrorEventMap.getOrDefault(what, MEDIA_ERROR_UNKNOWN);
+ cb.onError(MediaPlayer2Impl.this, mCurrentDSD, w, extra);
}
- }
+ });
return true;
}
});
@@ -1665,57 +1735,40 @@
processPendingTask_l();
}
}
- synchronized (mEventCbLock) {
- final long seekPos = getCurrentPosition();
- final int callbackCount = mPlayerEventCallbackMap.size();
- for (int i = 0; i < callbackCount; i++) {
- final Executor executor = mPlayerEventCallbackMap.valueAt(i);
- final PlayerEventCallback cb = mPlayerEventCallbackMap.keyAt(i);
- executor.execute(new Runnable() {
- @Override
- public void run() {
- // TODO: The actual seeked position might be different from the
- // requested position. Clarify which one is expected here.
- cb.onSeekCompleted(MediaPlayer2Impl.this, seekPos);
- }
- });
+ final long seekPos = getCurrentPosition();
+ notifyPlayerEvent(new PlayerEventNotifier() {
+ @Override
+ public void notify(PlayerEventCallback cb) {
+ // TODO: The actual seeked position might be different from the
+ // requested position. Clarify which one is expected here.
+ cb.onSeekCompleted(MediaPlayer2Impl.this, seekPos);
}
- }
+ });
}
});
mPlayer.setOnTimedMetaDataAvailableListener(
new MediaPlayer.OnTimedMetaDataAvailableListener() {
@Override
public void onTimedMetaDataAvailable(MediaPlayer mp, final TimedMetaData data) {
- synchronized (mEventCbLock) {
- for (final Pair<Executor, MediaPlayer2EventCallback> cb :
- mMp2EventCallbackRecords) {
- cb.first.execute(new Runnable() {
- @Override
- public void run() {
- cb.second.onTimedMetaDataAvailable(MediaPlayer2Impl.this,
- mCurrentDSD, data);
- }
- });
+ notifyMediaPlayer2Event(new Mp2EventNotifier() {
+ @Override
+ public void notify(MediaPlayer2EventCallback cb) {
+ cb.onTimedMetaDataAvailable(
+ MediaPlayer2Impl.this, mCurrentDSD, data);
}
- }
+ });
}
});
mPlayer.setOnInfoListener(new MediaPlayer.OnInfoListener() {
@Override
public boolean onInfo(MediaPlayer mp, final int what, final int extra) {
- synchronized (mEventCbLock) {
- for (final Pair<Executor, MediaPlayer2EventCallback> cb :
- mMp2EventCallbackRecords) {
- cb.first.execute(new Runnable() {
- @Override
- public void run() {
- // TODO: translate what value to MP2's definition.
- cb.second.onInfo(MediaPlayer2Impl.this, mCurrentDSD, what, extra);
- }
- });
+ notifyMediaPlayer2Event(new Mp2EventNotifier() {
+ @Override
+ public void notify(MediaPlayer2EventCallback cb) {
+ int w = sInfoEventMap.getOrDefault(what, MEDIA_INFO_UNKNOWN);
+ cb.onInfo(MediaPlayer2Impl.this, mCurrentDSD, w, extra);
}
- }
+ });
return true;
}
});
@@ -1725,19 +1778,64 @@
if (percent >= 100) {
setBufferingState(BUFFERING_STATE_BUFFERING_COMPLETE);
}
- synchronized (mEventCbLock) {
- mBufferedPercentageCurrent.set(percent);
- for (final Pair<Executor, MediaPlayer2EventCallback> cb :
- mMp2EventCallbackRecords) {
- cb.first.execute(new Runnable() {
+ mBufferedPercentageCurrent.set(percent);
+ notifyMediaPlayer2Event(new Mp2EventNotifier() {
+ @Override
+ public void notify(MediaPlayer2EventCallback cb) {
+ cb.onInfo(MediaPlayer2Impl.this, mCurrentDSD,
+ MEDIA_INFO_BUFFERING_UPDATE, percent);
+ }
+ });
+ }
+ });
+ mPlayer.setOnMediaTimeDiscontinuityListener(
+ new MediaPlayer.OnMediaTimeDiscontinuityListener() {
+ @Override
+ public void onMediaTimeDiscontinuity(
+ MediaPlayer mp, final MediaTimestamp timestamp) {
+ notifyMediaPlayer2Event(new Mp2EventNotifier() {
@Override
- public void run() {
- cb.second.onInfo(MediaPlayer2Impl.this, mCurrentDSD,
- MEDIA_INFO_BUFFERING_UPDATE, percent);
+ public void notify(MediaPlayer2EventCallback cb) {
+ cb.onMediaTimeDiscontinuity(
+ MediaPlayer2Impl.this, mCurrentDSD, timestamp);
}
});
}
- }
+ });
+ mPlayer.setOnSubtitleDataListener(new MediaPlayer.OnSubtitleDataListener() {
+ @Override
+ public void onSubtitleData(MediaPlayer mp, final SubtitleData data) {
+ notifyMediaPlayer2Event(new Mp2EventNotifier() {
+ @Override
+ public void notify(MediaPlayer2EventCallback cb) {
+ cb.onSubtitleData(MediaPlayer2Impl.this, mCurrentDSD, data);
+ }
+ });
+ }
+ });
+ mPlayer.setOnDrmInfoListener(new MediaPlayer.OnDrmInfoListener() {
+ @Override
+ public void onDrmInfo(MediaPlayer mp, final MediaPlayer.DrmInfo drmInfo) {
+ notifyDrmEvent(new DrmEventNotifier() {
+ @Override
+ public void notify(DrmEventCallback cb) {
+ cb.onDrmInfo(MediaPlayer2Impl.this, mCurrentDSD,
+ new DrmInfoImpl(drmInfo.getPssh(), drmInfo.getSupportedSchemes()));
+ }
+ });
+ }
+ });
+ mPlayer.setOnDrmPreparedListener(new MediaPlayer.OnDrmPreparedListener() {
+ @Override
+ public void onDrmPrepared(MediaPlayer mp, final int status) {
+ notifyDrmEvent(new DrmEventNotifier() {
+ @Override
+ public void notify(DrmEventCallback cb) {
+ int s = sPrepareDrmStatusMap.getOrDefault(
+ status, PREPARE_DRM_STATUS_PREPARATION_ERROR);
+ cb.onDrmPrepared(MediaPlayer2Impl.this, mCurrentDSD, s);
+ }
+ });
}
});
}
@@ -1968,18 +2066,13 @@
if (mMediaCallType == CALL_COMPLETED_NOTIFY_WHEN_COMMAND_LABEL_REACHED) {
return;
}
- synchronized (mEventCbLock) {
- for (final Pair<Executor, MediaPlayer2EventCallback> cb :
- mMp2EventCallbackRecords) {
- cb.first.execute(new Runnable() {
- @Override
- public void run() {
- cb.second.onCallCompleted(
- MediaPlayer2Impl.this, mDSD, mMediaCallType, status);
- }
- });
+ notifyMediaPlayer2Event(new Mp2EventNotifier() {
+ @Override
+ public void notify(MediaPlayer2EventCallback cb) {
+ cb.onCallCompleted(
+ MediaPlayer2Impl.this, mDSD, mMediaCallType, status);
}
- }
+ });
}
};
}
diff --git a/media/src/main/java/androidx/media/MediaPlayerBase.java b/media/src/main/java/androidx/media/MediaPlayerBase.java
index 6854ae0..de0e128 100644
--- a/media/src/main/java/androidx/media/MediaPlayerBase.java
+++ b/media/src/main/java/androidx/media/MediaPlayerBase.java
@@ -32,11 +32,9 @@
import java.util.concurrent.Executor;
/**
- * @hide
* Base class for all media players that want media session.
*/
@TargetApi(Build.VERSION_CODES.KITKAT)
-@RestrictTo(LIBRARY_GROUP)
public abstract class MediaPlayerBase implements AutoCloseable {
/**
* @hide
diff --git a/media/src/main/java/androidx/media/MediaPlaylistAgent.java b/media/src/main/java/androidx/media/MediaPlaylistAgent.java
index 802512d..07838e8 100644
--- a/media/src/main/java/androidx/media/MediaPlaylistAgent.java
+++ b/media/src/main/java/androidx/media/MediaPlaylistAgent.java
@@ -33,7 +33,6 @@
import java.util.concurrent.Executor;
/**
- * @hide
* MediaPlaylistAgent is the abstract class an application needs to derive from to pass an object
* to a MediaSession2 that will override default playlist handling behaviors. It contains a set of
* notify methods to signal MediaSession2 that playlist-related state has changed.
@@ -43,7 +42,6 @@
* Used by {@link MediaSession2} and {@link MediaController2}.
*/
// This class only includes methods that contain {@link MediaItem2}.
-@RestrictTo(LIBRARY_GROUP)
public abstract class MediaPlaylistAgent {
private static final String TAG = "MediaPlaylistAgent";
@@ -151,7 +149,11 @@
}
/**
- * TODO: add javadoc
+ * Notifies the current playlist and playlist metadata. Call this API when the playlist is
+ * changed.
+ * <p>
+ * Registered {@link PlaylistEventCallback} would receive this event through the
+ * {@link PlaylistEventCallback#onPlaylistChanged(MediaPlaylistAgent, List, MediaMetadata2)}.
*/
public final void notifyPlaylistChanged() {
SimpleArrayMap<PlaylistEventCallback, Executor> callbacks = getCallbacks();
@@ -171,7 +173,10 @@
}
/**
- * TODO: add javadoc
+ * Notifies the current playlist metadata. Call this API when the playlist metadata is changed.
+ * <p>
+ * Registered {@link PlaylistEventCallback} would receive this event through the
+ * {@link PlaylistEventCallback#onPlaylistMetadataChanged(MediaPlaylistAgent, MediaMetadata2)}.
*/
public final void notifyPlaylistMetadataChanged() {
SimpleArrayMap<PlaylistEventCallback, Executor> callbacks = getCallbacks();
@@ -189,7 +194,10 @@
}
/**
- * TODO: add javadoc
+ * Notifies the current shuffle mode. Call this API when the shuffle mode is changed.
+ * <p>
+ * Registered {@link PlaylistEventCallback} would receive this event through the
+ * {@link PlaylistEventCallback#onShuffleModeChanged(MediaPlaylistAgent, int)}.
*/
public final void notifyShuffleModeChanged() {
SimpleArrayMap<PlaylistEventCallback, Executor> callbacks = getCallbacks();
@@ -207,7 +215,10 @@
}
/**
- * TODO: add javadoc
+ * Notifies the current repeat mode. Call this API when the repeat mode is changed.
+ * <p>
+ * Registered {@link PlaylistEventCallback} would receive this event through the
+ * {@link PlaylistEventCallback#onRepeatModeChanged(MediaPlaylistAgent, int)}.
*/
public final void notifyRepeatModeChanged() {
SimpleArrayMap<PlaylistEventCallback, Executor> callbacks = getCallbacks();
@@ -232,10 +243,14 @@
public abstract @Nullable List<MediaItem2> getPlaylist();
/**
- * Sets the playlist.
+ * Sets the playlist with the metadata.
+ * <p>
+ * When the playlist is changed, call {@link #notifyPlaylistChanged()} to notify changes to the
+ * registered callbacks.
*
* @param list playlist
* @param metadata metadata of the playlist
+ * @see #notifyPlaylistChanged()
*/
public abstract void setPlaylist(@NonNull List<MediaItem2> list,
@Nullable MediaMetadata2 metadata);
@@ -248,9 +263,13 @@
public abstract @Nullable MediaMetadata2 getPlaylistMetadata();
/**
- * Updates the playlist metadata
+ * Updates the playlist metadata.
+ * <p>
+ * When the playlist metadata is changed, call {@link #notifyPlaylistMetadataChanged()} to
+ * notify changes to the registered callbacks.
*
* @param metadata metadata of the playlist
+ * @see #notifyPlaylistMetadataChanged()
*/
public abstract void updatePlaylistMetadata(@Nullable MediaMetadata2 metadata);
@@ -261,7 +280,8 @@
/**
* Adds the media item to the playlist at position index. Index equals or greater than
- * the current playlist size will add the item at the end of the playlist.
+ * the current playlist size (e.g. {@link Integer#MAX_VALUE}) will add the item at the end of
+ * the playlist.
* <p>
* This will not change the currently playing media item.
* If index is less than or equal to the current index of the playlist,
@@ -317,13 +337,17 @@
public abstract @RepeatMode int getRepeatMode();
/**
- * Sets the repeat mode
+ * Sets the repeat mode.
+ * <p>
+ * When the repeat mode is changed, call {@link #notifyRepeatModeChanged()} to notify changes
+ * to the registered callbacks.
*
* @param repeatMode repeat mode
* @see #REPEAT_MODE_NONE
* @see #REPEAT_MODE_ONE
* @see #REPEAT_MODE_ALL
* @see #REPEAT_MODE_GROUP
+ * @see #notifyRepeatModeChanged()
*/
public abstract void setRepeatMode(@RepeatMode int repeatMode);
@@ -338,12 +362,16 @@
public abstract @ShuffleMode int getShuffleMode();
/**
- * Sets the shuffle mode
+ * Sets the shuffle mode.
+ * <p>
+ * When the shuffle mode is changed, call {@link #notifyShuffleModeChanged()} to notify changes
+ * to the registered callbacks.
*
* @param shuffleMode The shuffle mode
* @see #SHUFFLE_MODE_NONE
* @see #SHUFFLE_MODE_ALL
* @see #SHUFFLE_MODE_GROUP
+ * @see #notifyShuffleModeChanged()
*/
public abstract void setShuffleMode(@ShuffleMode int shuffleMode);
diff --git a/media/src/main/java/androidx/media/MediaSession2.java b/media/src/main/java/androidx/media/MediaSession2.java
index 2ecb7ce..909e979 100644
--- a/media/src/main/java/androidx/media/MediaSession2.java
+++ b/media/src/main/java/androidx/media/MediaSession2.java
@@ -48,7 +48,6 @@
import java.util.concurrent.Executor;
/**
- * @hide
* Allows a media app to expose its transport controls and playback information in a process to
* other processes including the Android framework and other apps. Common use cases are as follows.
* <ul>
@@ -61,10 +60,6 @@
* handle media keys. In general an app only needs one session for all playback, though multiple
* sessions can be created to provide finer grain controls of media.
* <p>
- * If you want to support background playback, {@link MediaSessionService2} is preferred
- * instead. With it, your playback can be revived even after playback is finished. See
- * {@link MediaSessionService2} for details.
- * <p>
* A session can be obtained by {@link Builder}. The owner of the session may pass its session token
* to other processes to allow them to create a {@link MediaController2} to interact with the
* session.
@@ -77,11 +72,8 @@
* and notify any controllers.
* <p>
* {@link MediaSession2} objects should be used on the thread on the looper.
- *
- * @see MediaSessionService2
*/
@TargetApi(Build.VERSION_CODES.KITKAT)
-@RestrictTo(LIBRARY_GROUP)
public class MediaSession2 extends MediaInterface2.SessionPlayer implements AutoCloseable {
/**
* @hide
@@ -163,21 +155,18 @@
public static final int ERROR_CODE_SETUP_REQUIRED = 12;
/**
- * TODO: Fix {link DataSourceDesc}
* Interface definition of a callback to be invoked when a {@link MediaItem2} in the playlist
- * didn't have a {link DataSourceDesc} but it's needed now for preparing or playing it.
+ * didn't have a {@link DataSourceDesc} but it's needed now for preparing or playing it.
*
* #see #setOnDataSourceMissingHelper
*/
public interface OnDataSourceMissingHelper {
/**
- * TODO: Fix {link DataSourceDesc}
- * Called when a {@link MediaItem2} in the playlist didn't have a {link DataSourceDesc}
+ * Called when a {@link MediaItem2} in the playlist didn't have a {@link DataSourceDesc}
* but it's needed now for preparing or playing it. Returned data source descriptor will be
* sent to the player directly to prepare or play the contents.
* <p>
- * TODO: Fix {link DataSourceDesc}
- * An exception may be thrown if the returned {link DataSourceDesc} is duplicated in the
+ * An exception may be thrown if the returned {@link DataSourceDesc} is duplicated in the
* playlist, so items cannot be differentiated.
*
* @param session the session for this event
@@ -195,7 +184,6 @@
* If it's not set, the session will accept all controllers and all incoming commands by
* default.
*/
- // TODO(jaewan): Move this to updatable for default implementation (b/74091963)
public abstract static class SessionCallback {
/**
* Called when a controller is created for this session. Return allowed commands for
@@ -434,7 +422,7 @@
/**
* Called when a controller called {@link MediaController2#subscribeRoutesInfo()}
* Session app should notify the routes information by calling
- * {@link MediaSession2#notifyRoutesInfoChanged(ControllerInfo, List<Bundle>)}.
+ * {@link MediaSession2#notifyRoutesInfoChanged(ControllerInfo, List)}.
*
* @param session the session for this event
* @param controller controller information
@@ -472,7 +460,6 @@
* @param player the player for this event
* @param item new item
*/
- // TODO(jaewan): Use this (b/74316764)
public void onCurrentMediaItemChanged(@NonNull MediaSession2 session,
@NonNull MediaPlayerBase player, @NonNull MediaItem2 item) { }
@@ -788,7 +775,7 @@
public static final class ControllerInfo {
private final int mUid;
private final String mPackageName;
- // TODO: IMediaControllerCallback should be used only for MediaSession2ImplBase
+ // Note: IMediaControllerCallback should be used only for MediaSession2ImplBase
private final IMediaControllerCallback mIControllerCallback;
private final boolean mIsTrusted;
@@ -801,7 +788,7 @@
mUid = uid;
mPackageName = packageName;
mIControllerCallback = callback;
- mIsTrusted = isTrusted();
+ mIsTrusted = false;
}
/**
@@ -824,11 +811,11 @@
* command request.
*
* @return {@code true} if the controller is trusted.
+ * @hide
*/
- // TODO: Remove this API
+ @RestrictTo(LIBRARY_GROUP)
public boolean isTrusted() {
- //return mProvider.isTrusted_impl();
- return false;
+ return mIsTrusted;
}
IBinder getId() {
@@ -860,7 +847,6 @@
*/
@RestrictTo(LIBRARY_GROUP)
public @NonNull Bundle toBundle() {
- // TODO: Fill here.
return new Bundle();
}
@@ -870,7 +856,6 @@
*/
@RestrictTo(LIBRARY_GROUP)
public static @NonNull ControllerInfo fromBundle(@NonNull Context context, Bundle bundle) {
- // TODO: Fill here.
return new ControllerInfo(context, -1, -1, "TODO", null);
}
@@ -951,7 +936,7 @@
}
/**
- * Return whether it's enabled
+ * Return whether it's enabled.
*
* @return {@code true} if enabled. {@code false} otherwise.
*/
@@ -1008,7 +993,10 @@
private boolean mEnabled;
/**
- * TODO: javadoc
+ * Sets the {@link SessionCommand2} that would be sent to the session when the button
+ * is clicked.
+ *
+ * @param command session command
*/
public @NonNull Builder setCommand(@Nullable SessionCommand2 command) {
mCommand = command;
@@ -1016,7 +1004,13 @@
}
/**
- * TODO: javadoc
+ * Sets the bitmap-type (e.g. PNG) icon resource id of the button.
+ * <p>
+ * None bitmap type (e.g. VectorDrawabale) may cause unexpected behavior when it's sent
+ * to {@link MediaController2} app, so please avoid using it especially for the older
+ * platform (API < 21).
+ *
+ * @param resId resource id of the button
*/
public @NonNull Builder setIconResId(int resId) {
mIconResId = resId;
@@ -1024,7 +1018,9 @@
}
/**
- * TODO: javadoc
+ * Sets the display name of the button.
+ *
+ * @param displayName display name of the button
*/
public @NonNull Builder setDisplayName(@Nullable String displayName) {
mDisplayName = displayName;
@@ -1032,7 +1028,11 @@
}
/**
- * TODO: javadoc
+ * Sets whether the button is enabled. Can be {@code false} to indicate that the button
+ * should be shown but isn't clickable.
+ *
+ * @param enabled {@code true} if the button is enabled and ready.
+ * {@code false} otherwise.
*/
public @NonNull Builder setEnabled(boolean enabled) {
mEnabled = enabled;
@@ -1040,7 +1040,9 @@
}
/**
- * TODO: javadoc
+ * Sets the extras of the button.
+ *
+ * @param extras extras information of the button
*/
public @NonNull Builder setExtras(@Nullable Bundle extras) {
mExtras = extras;
@@ -1048,7 +1050,9 @@
}
/**
- * TODO: javadoc
+ * Builds the {@link CommandButton}.
+ *
+ * @return a new {@link CommandButton}
*/
public @NonNull CommandButton build() {
return new CommandButton(mCommand, mIconResId, mDisplayName, mExtras, mEnabled);
@@ -1170,7 +1174,9 @@
}
/**
- * TODO: add javadoc
+ * Returns the list of connected controller.
+ *
+ * @return list of {@link ControllerInfo}
*/
public @NonNull List<ControllerInfo> getConnectedControllers() {
return mImpl.getConnectedControllers();
@@ -1409,14 +1415,12 @@
* Sets the data source missing helper. Helper will be used to provide default implementation of
* {@link MediaPlaylistAgent} when it isn't set by developer.
* <p>
- * TODO: Fix {link DataSourceDesc}
* Default implementation of the {@link MediaPlaylistAgent} will call helper when a
- * {@link MediaItem2} in the playlist doesn't have a {link DataSourceDesc}. This may happen
+ * {@link MediaItem2} in the playlist doesn't have a {@link DataSourceDesc}. This may happen
* when
* <ul>
- * TODO: Fix {link DataSourceDesc}
* <li>{@link MediaItem2} specified by {@link #setPlaylist(List, MediaMetadata2)} doesn't
- * have {link DataSourceDesc}</li>
+ * have {@link DataSourceDesc}</li>
* <li>{@link MediaController2#addPlaylistItem(int, MediaItem2)} is called and accepted
* by {@link SessionCallback#onCommandRequest(
* MediaSession2, ControllerInfo, SessionCommand2)}.
@@ -1477,8 +1481,7 @@
* list. Wait for {@link SessionCallback#onPlaylistChanged(MediaSession2, MediaPlaylistAgent,
* List, MediaMetadata2)} to know the operation finishes.
* <p>
- * TODO: Fix {link DataSourceDesc}
- * You may specify a {@link MediaItem2} without {link DataSourceDesc}. In that case,
+ * You may specify a {@link MediaItem2} without {@link DataSourceDesc}. In that case,
* {@link MediaPlaylistAgent} has responsibility to dynamically query {link DataSourceDesc}
* when such media item is ready for preparation or play. Default implementation needs
* {@link OnDataSourceMissingHelper} for such case.
@@ -1551,7 +1554,8 @@
/**
* Adds the media item to the playlist at position index. Index equals or greater than
- * the current playlist size will add the item at the end of the playlist.
+ * the current playlist size (e.g. {@link Integer#MAX_VALUE}) will add the item at the end of
+ * the playlist.
* <p>
* This will not change the currently playing media item.
* If index is less than or equal to the current index of the play list,
diff --git a/media/src/main/java/androidx/media/MediaSession2ImplBase.java b/media/src/main/java/androidx/media/MediaSession2ImplBase.java
index 989475a..e474b45 100644
--- a/media/src/main/java/androidx/media/MediaSession2ImplBase.java
+++ b/media/src/main/java/androidx/media/MediaSession2ImplBase.java
@@ -60,7 +60,7 @@
@TargetApi(Build.VERSION_CODES.KITKAT)
class MediaSession2ImplBase extends MediaSession2.SupportLibraryImpl {
static final String TAG = "MS2ImplBase";
- static final boolean DEBUG = true; // TODO: Log.isLoggable(TAG, Log.DEBUG);
+ static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
private final Object mLock = new Object();
diff --git a/media/src/main/java/androidx/media/MediaSession2StubImplBase.java b/media/src/main/java/androidx/media/MediaSession2StubImplBase.java
index 5fb31b7..48e641e 100644
--- a/media/src/main/java/androidx/media/MediaSession2StubImplBase.java
+++ b/media/src/main/java/androidx/media/MediaSession2StubImplBase.java
@@ -75,6 +75,7 @@
import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_SEEK_TO;
import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_SET_SPEED;
import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_ADD_ITEM;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_GET_CURRENT_MEDIA_ITEM;
import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_GET_LIST;
import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_REMOVE_ITEM;
import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_REPLACE_ITEM;
@@ -131,7 +132,7 @@
class MediaSession2StubImplBase extends MediaSessionCompat.Callback {
private static final String TAG = "MS2StubImplBase";
- private static final boolean DEBUG = true; // TODO: Log.isLoggable(TAG, Log.DEBUG);
+ private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
private static final SparseArray<SessionCommand2> sCommandsForOnCommandRequest =
new SparseArray<>();
@@ -531,7 +532,7 @@
}
void notifyCurrentMediaItemChanged(final MediaItem2 item) {
- notifyAll(COMMAND_CODE_PLAYLIST_GET_LIST, new Session2Runnable() {
+ notifyAll(COMMAND_CODE_PLAYLIST_GET_CURRENT_MEDIA_ITEM, new Session2Runnable() {
@Override
public void run(ControllerInfo controller) throws RemoteException {
Bundle bundle = new Bundle();
@@ -622,7 +623,7 @@
void notifyPlaylistChanged(final List<MediaItem2> playlist,
final MediaMetadata2 metadata) {
- notifyAll(SessionCommand2.COMMAND_CODE_PLAYLIST_GET_LIST, new Session2Runnable() {
+ notifyAll(COMMAND_CODE_PLAYLIST_GET_LIST, new Session2Runnable() {
@Override
public void run(ControllerInfo controller) throws RemoteException {
Bundle bundle = new Bundle();
@@ -866,29 +867,29 @@
allowedCommands.toBundle());
resultData.putInt(ARGUMENT_PLAYER_STATE, mSession.getPlayerState());
resultData.putInt(ARGUMENT_BUFFERING_STATE, mSession.getBufferingState());
- synchronized (mLock) {
- resultData.putParcelable(ARGUMENT_PLAYBACK_STATE_COMPAT,
- mSession.getPlaybackStateCompat());
- // TODO: Insert MediaMetadataCompat
- }
+ resultData.putParcelable(ARGUMENT_PLAYBACK_STATE_COMPAT,
+ mSession.getPlaybackStateCompat());
resultData.putInt(ARGUMENT_REPEAT_MODE, mSession.getRepeatMode());
resultData.putInt(ARGUMENT_SHUFFLE_MODE, mSession.getShuffleMode());
final List<MediaItem2> playlist = allowedCommands.hasCommand(
- SessionCommand2.COMMAND_CODE_PLAYLIST_GET_LIST)
- ? mSession.getPlaylist() : null;
+ COMMAND_CODE_PLAYLIST_GET_LIST) ? mSession.getPlaylist() : null;
if (playlist != null) {
resultData.putParcelableArray(ARGUMENT_PLAYLIST,
MediaUtils2.toMediaItem2ParcelableArray(playlist));
}
final MediaItem2 currentMediaItem =
- allowedCommands.hasCommand(COMMAND_CODE_PLAYLIST_GET_LIST)
+ allowedCommands.hasCommand(COMMAND_CODE_PLAYLIST_GET_CURRENT_MEDIA_ITEM)
? mSession.getCurrentMediaItem() : null;
if (currentMediaItem != null) {
resultData.putBundle(ARGUMENT_MEDIA_ITEM, currentMediaItem.toBundle());
}
resultData.putBundle(ARGUMENT_PLAYBACK_INFO,
mSession.getPlaybackInfo().toBundle());
-
+ final MediaMetadata2 playlistMetadata = mSession.getPlaylistMetadata();
+ if (playlistMetadata != null) {
+ resultData.putBundle(ARGUMENT_PLAYLIST_METADATA,
+ playlistMetadata.toBundle());
+ }
// Double check if session is still there, because close() can be
// called in another thread.
if (mSession.isClosed()) {
diff --git a/media/src/main/java/androidx/media/Rating2.java b/media/src/main/java/androidx/media/Rating2.java
index 28f7c70..8c81331 100644
--- a/media/src/main/java/androidx/media/Rating2.java
+++ b/media/src/main/java/androidx/media/Rating2.java
@@ -30,7 +30,6 @@
import java.lang.annotation.RetentionPolicy;
/**
- * @hide
* A class to encapsulate rating information used as content metadata.
* A rating is defined by its rating style (see {@link #RATING_HEART},
* {@link #RATING_THUMB_UP_DOWN}, {@link #RATING_3_STARS}, {@link #RATING_4_STARS},
@@ -40,7 +39,6 @@
*/
// New version of Rating with following change
// - Don't implement Parcelable for updatable support.
-@RestrictTo(LIBRARY_GROUP)
public final class Rating2 {
/**
* @hide
diff --git a/media/src/main/java/androidx/media/SessionCommand2.java b/media/src/main/java/androidx/media/SessionCommand2.java
index aca8234..f017941 100644
--- a/media/src/main/java/androidx/media/SessionCommand2.java
+++ b/media/src/main/java/androidx/media/SessionCommand2.java
@@ -31,14 +31,12 @@
import java.util.List;
/**
- * @hide
* Define a command that a {@link MediaController2} can send to a {@link MediaSession2}.
* <p>
* If {@link #getCommandCode()} isn't {@link #COMMAND_CODE_CUSTOM}), it's predefined command.
* If {@link #getCommandCode()} is {@link #COMMAND_CODE_CUSTOM}), it's custom command and
* {@link #getCustomCommand()} shouldn't be {@code null}.
*/
-@RestrictTo(LIBRARY_GROUP)
public final class SessionCommand2 {
/**
* Command code for the custom command which can be defined by string action in the
@@ -194,10 +192,6 @@
/**
* Command code for {@link MediaController2#getPlaylist()}. This will expose metadata
* information to the controller.
- * <p>
- * Command would be sent directly to the playlist agent if the session doesn't reject the
- * request through the
- * {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo, SessionCommand2)}.
*/
public static final int COMMAND_CODE_PLAYLIST_GET_LIST = 18;
@@ -213,10 +207,6 @@
/**
* Command code for {@link MediaController2#getPlaylistMetadata()}. This will expose
* metadata information to the controller.
- * <p>
- * Command would be sent directly to the playlist agent if the session doesn't reject the
- * request through the
- * {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo, SessionCommand2)}.
*/
public static final int COMMAND_CODE_PLAYLIST_GET_LIST_METADATA = 20;
@@ -230,6 +220,12 @@
public static final int COMMAND_CODE_PLAYLIST_SET_LIST_METADATA = 21;
/**
+ * Command code for {@link MediaController2#getCurrentMediaItem()}. This will expose
+ * metadata information to the controller.
+ */
+ public static final int COMMAND_CODE_PLAYLIST_GET_CURRENT_MEDIA_ITEM = 20;
+
+ /**
* Command code for {@link MediaController2#playFromMediaId(String, Bundle)}.
*/
public static final int COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID = 22;
@@ -279,40 +275,53 @@
*/
public static final int COMMAND_CODE_SESSION_SELECT_ROUTE = 38;
-
/**
+ * @hide
* Command code for {@link MediaBrowser2#getChildren(String, int, int, Bundle)}.
*/
+ @RestrictTo(LIBRARY_GROUP)
public static final int COMMAND_CODE_LIBRARY_GET_CHILDREN = 29;
/**
+ * @hide
* Command code for {@link MediaBrowser2#getItem(String)}.
*/
+ @RestrictTo(LIBRARY_GROUP)
public static final int COMMAND_CODE_LIBRARY_GET_ITEM = 30;
/**
+ * @hide
* Command code for {@link MediaBrowser2#getLibraryRoot(Bundle)}.
*/
+ @RestrictTo(LIBRARY_GROUP)
public static final int COMMAND_CODE_LIBRARY_GET_LIBRARY_ROOT = 31;
/**
+ * @hide
* Command code for {@link MediaBrowser2#getSearchResult(String, int, int, Bundle)}.
*/
+ @RestrictTo(LIBRARY_GROUP)
public static final int COMMAND_CODE_LIBRARY_GET_SEARCH_RESULT = 32;
/**
+ * @hide
* Command code for {@link MediaBrowser2#search(String, Bundle)}.
*/
+ @RestrictTo(LIBRARY_GROUP)
public static final int COMMAND_CODE_LIBRARY_SEARCH = 33;
/**
+ * @hide
* Command code for {@link MediaBrowser2#subscribe(String, Bundle)}.
*/
+ @RestrictTo(LIBRARY_GROUP)
public static final int COMMAND_CODE_LIBRARY_SUBSCRIBE = 34;
/**
+ * @hide
* Command code for {@link MediaBrowser2#unsubscribe(String)}.
*/
+ @RestrictTo(LIBRARY_GROUP)
public static final int COMMAND_CODE_LIBRARY_UNSUBSCRIBE = 35;
/**
@@ -429,7 +438,6 @@
return false;
}
SessionCommand2 other = (SessionCommand2) obj;
- // TODO(jaewan): Compare Commands with the generated UUID, as we're doing for the MI2.
return mCommandCode == other.mCommandCode
&& TextUtils.equals(mCustomCommand, other.mCustomCommand);
}
diff --git a/media/src/main/java/androidx/media/SessionCommandGroup2.java b/media/src/main/java/androidx/media/SessionCommandGroup2.java
index af4d189..691eb70 100644
--- a/media/src/main/java/androidx/media/SessionCommandGroup2.java
+++ b/media/src/main/java/androidx/media/SessionCommandGroup2.java
@@ -34,10 +34,8 @@
import java.util.Set;
/**
- * @hide
* A set of {@link SessionCommand2} which represents a command group.
*/
-@RestrictTo(LIBRARY_GROUP)
public final class SessionCommandGroup2 {
private static final String TAG = "SessionCommandGroup2";
@@ -114,7 +112,6 @@
}
private void addCommandsWithPrefix(String prefix) {
- // TODO(jaewan): (Can be post-P): Don't use reflection for this purpose.
final Field[] fields = SessionCommand2.class.getFields();
if (fields != null) {
for (int i = 0; i < fields.length; i++) {
diff --git a/media/src/main/java/androidx/media/SessionToken2.java b/media/src/main/java/androidx/media/SessionToken2.java
index 6b6021a..eb42297 100644
--- a/media/src/main/java/androidx/media/SessionToken2.java
+++ b/media/src/main/java/androidx/media/SessionToken2.java
@@ -30,6 +30,7 @@
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import java.lang.annotation.Retention;
@@ -37,9 +38,7 @@
import java.util.List;
/**
- * @hide
- * Represents an ongoing {@link MediaSession2} or a {@link MediaSessionService2}.
- * If it's representing a session service, it may not be ongoing.
+ * Represents an ongoing {@link MediaSession2}.
* <p>
* This may be passed to apps by the session owner to allow them to create a
* {@link MediaController2} to communicate with the session.
@@ -50,15 +49,31 @@
// - Stop implementing Parcelable for updatable support
// - Represent session and library service (formerly browser service) in one class.
// Previously MediaSession.Token was for session and ComponentName was for service.
-@RestrictTo(LIBRARY_GROUP)
public final class SessionToken2 {
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
@Retention(RetentionPolicy.SOURCE)
@IntDef(value = {TYPE_SESSION, TYPE_SESSION_SERVICE, TYPE_LIBRARY_SERVICE})
public @interface TokenType {
}
+ /**
+ * Type for {@link MediaSession2}.
+ */
public static final int TYPE_SESSION = 0;
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
public static final int TYPE_SESSION_SERVICE = 1;
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
public static final int TYPE_LIBRARY_SERVICE = 2;
//private final SessionToken2Provider mProvider;
@@ -79,14 +94,17 @@
private final String mServiceName;
private final String mId;
private final MediaSessionCompat.Token mSessionCompatToken;
+ private final ComponentName mComponentName;
/**
+ * @hide
* Constructor for the token. You can only create token for session service or library service
* to use by {@link MediaController2} or {@link MediaBrowser2}.
*
* @param context The context.
* @param serviceComponent The component name of the media browser service.
*/
+ @RestrictTo(LIBRARY_GROUP)
public SessionToken2(@NonNull Context context, @NonNull ComponentName serviceComponent) {
this(context, serviceComponent, UID_UNKNOWN);
}
@@ -106,6 +124,7 @@
if (serviceComponent == null) {
throw new IllegalArgumentException("serviceComponent shouldn't be null");
}
+ mComponentName = serviceComponent;
mPackageName = serviceComponent.getPackageName();
mServiceName = serviceComponent.getClassName();
// Calculate uid if it's not specified.
@@ -117,11 +136,9 @@
throw new IllegalArgumentException("Cannot find package " + mPackageName);
}
}
-
mUid = uid;
// Infer id and type from package name and service name
- // TODO(jaewan): Handle multi-user.
String id = getSessionIdFromService(manager, MediaLibraryService2.SERVICE_INTERFACE,
serviceComponent);
if (id != null) {
@@ -144,12 +161,14 @@
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
- public SessionToken2(int uid, int type, String packageName, String serviceName,
+ SessionToken2(int uid, int type, String packageName, String serviceName,
String id, MediaSessionCompat.Token sessionCompatToken) {
mUid = uid;
mType = type;
mPackageName = packageName;
mServiceName = serviceName;
+ mComponentName = (mType == TYPE_SESSION) ? null
+ : new ComponentName(packageName, serviceName);
mId = id;
mSessionCompatToken = sessionCompatToken;
}
@@ -193,14 +212,14 @@
/**
* @return package name
*/
- public String getPackageName() {
+ public @NonNull String getPackageName() {
return mPackageName;
}
/**
- * @return service name
+ * @return service name. Can be {@code null} for TYPE_SESSION.
*/
- public String getServiceName() {
+ public @Nullable String getServiceName() {
return mServiceName;
}
@@ -210,8 +229,7 @@
*/
@RestrictTo(LIBRARY_GROUP)
public ComponentName getComponentName() {
- // TODO: Cache the component name?
- return mType == TYPE_SESSION ? null : new ComponentName(mPackageName, mServiceName);
+ return mComponentName;
}
/**
@@ -224,7 +242,6 @@
/**
* @return type of the token
* @see #TYPE_SESSION
- * @see #TYPE_SESSION_SERVICE
*/
public @TokenType int getType() {
return mType;
diff --git a/samples/SupportSliceDemos/build.gradle b/samples/SupportSliceDemos/build.gradle
index 4d518ff..8675869 100644
--- a/samples/SupportSliceDemos/build.gradle
+++ b/samples/SupportSliceDemos/build.gradle
@@ -21,9 +21,9 @@
}
dependencies {
- implementation(project(":slices-view"))
- implementation(project(":slices-builders"))
- implementation(project(":slices-core"))
+ implementation(project(":slice-view"))
+ implementation(project(":slice-builders"))
+ implementation(project(":slice-core"))
implementation("com.android.support:design:28.0.0-SNAPSHOT", { transitive = false })
implementation(project(":transition"))
implementation(project(":recyclerview"))
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 4facfb3..f1e2976 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
@@ -16,6 +16,8 @@
package com.example.androidx.slice.demos;
+import static androidx.slice.core.SliceHints.INFINITY;
+
import static com.example.androidx.slice.demos.SampleSliceProvider.URI_PATHS;
import static com.example.androidx.slice.demos.SampleSliceProvider.getUri;
@@ -48,6 +50,7 @@
import androidx.lifecycle.LiveData;
import androidx.slice.Slice;
import androidx.slice.SliceItem;
+import androidx.slice.SliceMetadata;
import androidx.slice.widget.EventInfo;
import androidx.slice.widget.SliceLiveData;
import androidx.slice.widget.SliceView;
@@ -239,7 +242,17 @@
mContainer.addView(v);
mSliceLiveData = SliceLiveData.fromUri(this, uri);
v.setMode(mSelectedMode);
- mSliceLiveData.observe(this, v);
+ mSliceLiveData.observe(this, slice -> {
+ v.setSlice(slice);
+ SliceMetadata metadata = SliceMetadata.from(this, slice);
+ long expiry = metadata.getExpiry();
+ if (expiry != INFINITY) {
+ // Shows the updated text after the TTL expires.
+ v.postDelayed(() -> v.setSlice(slice),
+ expiry - System.currentTimeMillis() + 15);
+ }
+ });
+ mSliceLiveData.observe(this, slice -> Log.d(TAG, "Slice: " + slice));
} else {
Log.w(TAG, "Invalid uri, skipping slice: " + uri);
}
diff --git a/settings.gradle b/settings.gradle
index 65ef3f4..16e5be7 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -77,9 +77,9 @@
includeProject(":recommendation", "recommendation")
includeProject(":recyclerview", "v7/recyclerview")
includeProject(":recyclerview-selection", "recyclerview-selection")
-includeProject(":slices-core", "slices/core")
-includeProject(":slices-view", "slices/view")
-includeProject(":slices-builders", "slices/builders")
+includeProject(":slice-core", "slices/core")
+includeProject(":slice-view", "slices/view")
+includeProject(":slice-builders", "slices/builders")
includeProject(":slidingpanelayout", "slidingpanelayout")
includeProject(":fragment-ktx", "fragment/ktx")
includeProject(":swiperefreshlayout", "swiperefreshlayout")
diff --git a/slices/builders/build.gradle b/slices/builders/build.gradle
index a9624b2..91c7599 100644
--- a/slices/builders/build.gradle
+++ b/slices/builders/build.gradle
@@ -23,7 +23,7 @@
}
dependencies {
- implementation(project(":slices-core"))
+ implementation(project(":slice-core"))
implementation project(":annotation")
implementation project(path: ':core')
}
diff --git a/slices/core/src/main/java/androidx/slice/Slice.java b/slices/core/src/main/java/androidx/slice/Slice.java
index d18ef23..a2c3325 100644
--- a/slices/core/src/main/java/androidx/slice/Slice.java
+++ b/slices/core/src/main/java/androidx/slice/Slice.java
@@ -455,20 +455,37 @@
public String toString(String indent) {
StringBuilder sb = new StringBuilder();
sb.append(indent);
- sb.append("slice: ");
- sb.append("\n");
- indent += " ";
+ sb.append("slice ");
+ addHints(sb, mHints);
+ sb.append("{\n");
+ String nextIndent = indent + " ";
for (int i = 0; i < mItems.length; i++) {
SliceItem item = mItems[i];
- sb.append(item.toString(indent));
- if (!FORMAT_SLICE.equals(item.getFormat())) {
- sb.append("\n");
- }
+ sb.append(item.toString(nextIndent));
}
+ sb.append(indent);
+ sb.append("}");
return sb.toString();
}
/**
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY)
+ public static void addHints(StringBuilder sb, String[] hints) {
+ if (hints.length == 0) return;
+
+ sb.append("(");
+ int end = hints.length - 1;
+ for (int i = 0; i < end; i++) {
+ sb.append(hints[i]);
+ sb.append(", ");
+ }
+ sb.append(hints[end]);
+ sb.append(") ");
+ }
+
+ /**
* Turns a slice Uri into slice content.
*
* @hide
diff --git a/slices/core/src/main/java/androidx/slice/SliceItem.java b/slices/core/src/main/java/androidx/slice/SliceItem.java
index 004e51d..964c5df 100644
--- a/slices/core/src/main/java/androidx/slice/SliceItem.java
+++ b/slices/core/src/main/java/androidx/slice/SliceItem.java
@@ -25,6 +25,8 @@
import static android.app.slice.SliceItem.FORMAT_TEXT;
import static android.app.slice.SliceItem.FORMAT_TIMESTAMP;
+import static androidx.slice.Slice.addHints;
+
import android.app.PendingIntent;
import android.app.RemoteInput;
import android.content.Context;
@@ -412,36 +414,35 @@
@RestrictTo(Scope.LIBRARY)
public String toString(String indent) {
StringBuilder sb = new StringBuilder();
- if (!FORMAT_SLICE.equals(getFormat())) {
- sb.append(indent);
- sb.append(getFormat());
- sb.append(": ");
- }
switch (getFormat()) {
case FORMAT_SLICE:
sb.append(getSlice().toString(indent));
break;
case FORMAT_ACTION:
- sb.append(getAction());
- sb.append("\n");
+ sb.append(indent).append(getAction()).append(",\n");
sb.append(getSlice().toString(indent));
break;
case FORMAT_TEXT:
- sb.append(getText());
+ sb.append(indent).append('"').append(getText()).append('"');
break;
case FORMAT_IMAGE:
- sb.append(getIcon());
+ sb.append(indent).append(getIcon());
break;
case FORMAT_INT:
- sb.append(getInt());
+ sb.append(indent).append(getInt());
break;
- case FORMAT_TIMESTAMP:
- sb.append(getTimestamp());
+ case FORMAT_LONG:
+ sb.append(indent).append(getLong());
break;
default:
- sb.append(SliceItem.typeToString(getFormat()));
+ sb.append(indent).append(SliceItem.typeToString(getFormat()));
break;
}
+ if (!FORMAT_SLICE.equals(getFormat())) {
+ sb.append(' ');
+ addHints(sb, mHints);
+ }
+ sb.append(",\n");
return sb.toString();
}
}
diff --git a/slices/core/src/main/java/androidx/slice/SliceProvider.java b/slices/core/src/main/java/androidx/slice/SliceProvider.java
index 7ec9232..9061ab1 100644
--- a/slices/core/src/main/java/androidx/slice/SliceProvider.java
+++ b/slices/core/src/main/java/androidx/slice/SliceProvider.java
@@ -20,20 +20,8 @@
import static android.app.slice.SliceProvider.SLICE_TYPE;
import static androidx.slice.compat.SliceProviderCompat.EXTRA_BIND_URI;
-import static androidx.slice.compat.SliceProviderCompat.EXTRA_INTENT;
import static androidx.slice.compat.SliceProviderCompat.EXTRA_PKG;
import static androidx.slice.compat.SliceProviderCompat.EXTRA_PROVIDER_PKG;
-import static androidx.slice.compat.SliceProviderCompat.EXTRA_SLICE;
-import static androidx.slice.compat.SliceProviderCompat.EXTRA_SLICE_DESCENDANTS;
-import static androidx.slice.compat.SliceProviderCompat.METHOD_GET_DESCENDANTS;
-import static androidx.slice.compat.SliceProviderCompat.METHOD_GET_PINNED_SPECS;
-import static androidx.slice.compat.SliceProviderCompat.METHOD_MAP_INTENT;
-import static androidx.slice.compat.SliceProviderCompat.METHOD_MAP_ONLY_INTENT;
-import static androidx.slice.compat.SliceProviderCompat.METHOD_PIN;
-import static androidx.slice.compat.SliceProviderCompat.METHOD_SLICE;
-import static androidx.slice.compat.SliceProviderCompat.METHOD_UNPIN;
-import static androidx.slice.compat.SliceProviderCompat.addSpecs;
-import static androidx.slice.compat.SliceProviderCompat.getSpecs;
import static androidx.slice.core.SliceHints.HINT_PERMISSION_REQUEST;
import android.app.PendingIntent;
@@ -48,13 +36,8 @@
import android.database.ContentObserver;
import android.database.Cursor;
import android.net.Uri;
-import android.os.Binder;
import android.os.Bundle;
import android.os.CancellationSignal;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.Process;
-import android.os.StrictMode;
import android.util.Log;
import androidx.annotation.NonNull;
@@ -63,11 +46,10 @@
import androidx.annotation.RestrictTo;
import androidx.core.app.CoreComponentFactory;
import androidx.core.os.BuildCompat;
-import androidx.slice.compat.CompatPinnedList;
+import androidx.slice.compat.SliceProviderCompat;
import androidx.slice.compat.SliceProviderWrapperContainer;
import androidx.slice.core.R;
-import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Set;
@@ -118,14 +100,9 @@
private static final String TAG = "SliceProvider";
- private static final String DATA_PREFIX = "slice_data_";
- private static final long SLICE_BIND_ANR = 2000;
-
private static final boolean DEBUG = false;
- private final Handler mHandler = new Handler(Looper.getMainLooper());
- private CompatPinnedList mPinnedList;
- private String mCallback;
+ private SliceProviderCompat mCompat;
/**
* Implement this to initialize your slice provider on startup.
@@ -154,8 +131,9 @@
@Override
public final boolean onCreate() {
- mPinnedList = new CompatPinnedList(getContext(),
- DATA_PREFIX + getClass().getName());
+ if (!BuildCompat.isAtLeastP()) {
+ mCompat = new SliceProviderCompat(this);
+ }
return onCreateSliceProvider();
}
@@ -167,115 +145,7 @@
@Override
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());
- Bundle b = new Bundle();
- b.putParcelable(EXTRA_SLICE, s.toBundle());
- return b;
- } else if (method.equals(METHOD_MAP_INTENT)) {
- Intent intent = extras.getParcelable(EXTRA_INTENT);
- Uri uri = onMapIntentToUri(intent);
- Bundle b = new Bundle();
- if (uri != null) {
- Set<SliceSpec> specs = getSpecs(extras);
- Slice s = handleBindSlice(uri, specs, getCallingPackage());
- b.putParcelable(EXTRA_SLICE, s.toBundle());
- } else {
- b.putParcelable(EXTRA_SLICE, null);
- }
- return b;
- } else if (method.equals(METHOD_MAP_ONLY_INTENT)) {
- Intent intent = extras.getParcelable(EXTRA_INTENT);
- Uri uri = onMapIntentToUri(intent);
- Bundle b = new Bundle();
- b.putParcelable(EXTRA_SLICE, uri);
- return b;
- } else if (method.equals(METHOD_PIN)) {
- Uri uri = extras.getParcelable(EXTRA_BIND_URI);
- Set<SliceSpec> specs = getSpecs(extras);
- String pkg = extras.getString(EXTRA_PKG);
- if (mPinnedList.addPin(uri, pkg, specs)) {
- handleSlicePinned(uri);
- }
- return null;
- } else if (method.equals(METHOD_UNPIN)) {
- Uri uri = extras.getParcelable(EXTRA_BIND_URI);
- String pkg = extras.getString(EXTRA_PKG);
- if (mPinnedList.removePin(uri, pkg)) {
- handleSliceUnpinned(uri);
- }
- return null;
- } else if (method.equals(METHOD_GET_PINNED_SPECS)) {
- Uri uri = extras.getParcelable(EXTRA_BIND_URI);
- Bundle b = new Bundle();
- addSpecs(b, mPinnedList.getSpecs(uri));
- return b;
- } else if (method.equals(METHOD_GET_DESCENDANTS)) {
- Uri uri = extras.getParcelable(EXTRA_BIND_URI);
- Bundle b = new Bundle();
- b.putParcelableArrayList(EXTRA_SLICE_DESCENDANTS,
- new ArrayList<>(handleGetDescendants(uri)));
- return b;
- }
- return super.call(method, arg, extras);
- }
-
- private Collection<Uri> handleGetDescendants(Uri uri) {
- mCallback = "onGetSliceDescendants";
- mHandler.postDelayed(mAnr, SLICE_BIND_ANR);
- try {
- return onGetSliceDescendants(uri);
- } finally {
- mHandler.removeCallbacks(mAnr);
- }
- }
-
- private void handleSlicePinned(final Uri sliceUri) {
- mCallback = "onSlicePinned";
- mHandler.postDelayed(mAnr, SLICE_BIND_ANR);
- try {
- onSlicePinned(sliceUri);
- } finally {
- mHandler.removeCallbacks(mAnr);
- }
- }
-
- private void handleSliceUnpinned(final Uri sliceUri) {
- mCallback = "onSliceUnpinned";
- mHandler.postDelayed(mAnr, SLICE_BIND_ANR);
- try {
- onSliceUnpinned(sliceUri);
- } finally {
- mHandler.removeCallbacks(mAnr);
- }
- }
-
- private Slice handleBindSlice(final Uri sliceUri, final Set<SliceSpec> specs,
- final String callingPkg) {
- // This can be removed once Slice#bindSlice is removed and everyone is using
- // SliceManager#bindSlice.
- String pkg = callingPkg != null ? callingPkg
- : getContext().getPackageManager().getNameForUid(Binder.getCallingUid());
- if (Binder.getCallingUid() != Process.myUid()) {
- try {
- getContext().enforceUriPermission(sliceUri,
- Binder.getCallingPid(), Binder.getCallingUid(),
- Intent.FLAG_GRANT_WRITE_URI_PERMISSION,
- "Slice binding requires write access to Uri");
- } catch (SecurityException e) {
- return createPermissionSlice(getContext(), sliceUri, pkg);
- }
- }
- return onBindSliceStrict(sliceUri, specs);
+ return mCompat != null ? mCompat.call(method, arg, extras) : null;
}
/**
@@ -337,35 +207,6 @@
}
}
- private Slice onBindSliceStrict(Uri sliceUri, Set<SliceSpec> specs) {
- StrictMode.ThreadPolicy oldPolicy = StrictMode.getThreadPolicy();
- mCallback = "onBindSlice";
- mHandler.postDelayed(mAnr, SLICE_BIND_ANR);
- try {
- StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
- .detectAll()
- .penaltyDeath()
- .build());
- SliceProvider.setSpecs(specs);
- try {
- return onBindSlice(sliceUri);
- } finally {
- SliceProvider.setSpecs(null);
- mHandler.removeCallbacks(mAnr);
- }
- } finally {
- StrictMode.setThreadPolicy(oldPolicy);
- }
- }
-
- private final Runnable mAnr = new Runnable() {
- @Override
- public void run() {
- Process.sendSignal(Process.myPid(), Process.SIGNAL_QUIT);
- Log.wtf(TAG, "Timed out while handling slice callback " + mCallback);
- }
- };
-
/**
* Implemented to create a slice.
* <p>
diff --git a/slices/core/src/main/java/androidx/slice/compat/CompatPermissionManager.java b/slices/core/src/main/java/androidx/slice/compat/CompatPermissionManager.java
new file mode 100644
index 0000000..247a763
--- /dev/null
+++ b/slices/core/src/main/java/androidx/slice/compat/CompatPermissionManager.java
@@ -0,0 +1,198 @@
+/*
+ * 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.slice.compat;
+
+import static androidx.core.content.PermissionChecker.PERMISSION_DENIED;
+import static androidx.core.content.PermissionChecker.PERMISSION_GRANTED;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.collection.ArraySet;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+class CompatPermissionManager {
+
+ private static final String TAG = "CompatPermissionManager";
+ public static final String ALL_SUFFIX = "_all";
+
+ private final Context mContext;
+ private final String mPrefsName;
+
+ CompatPermissionManager(Context context, String prefsName) {
+ mContext = context;
+ mPrefsName = prefsName;
+ }
+
+ private SharedPreferences getPrefs() {
+ return mContext.getSharedPreferences(mPrefsName, Context.MODE_PRIVATE);
+ }
+
+ public int checkSlicePermission(Uri uri, int pid, int uid) {
+ for (String pkg : mContext.getPackageManager().getPackagesForUid(uid)) {
+ if (checkSlicePermission(uri, pkg) == PERMISSION_GRANTED) {
+ return PERMISSION_GRANTED;
+ }
+ }
+ // Fall back to allowing uri permissions through.
+ return mContext.checkUriPermission(uri, pid, uid, Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+ }
+
+ private int checkSlicePermission(Uri uri, String pkg) {
+ PermissionState state = getPermissionState(pkg, uri.getAuthority());
+ return state.hasAccess(uri.getPathSegments()) ? PERMISSION_GRANTED : PERMISSION_DENIED;
+ }
+
+ public void grantSlicePermission(Uri uri, String toPkg) {
+ PermissionState state = getPermissionState(toPkg, uri.getAuthority());
+ if (state.addPath(uri.getPathSegments())) {
+ persist(state);
+ }
+ }
+
+ public void revokeSlicePermission(Uri uri, String toPkg) {
+ PermissionState state = getPermissionState(toPkg, uri.getAuthority());
+ if (state.removePath(uri.getPathSegments())) {
+ persist(state);
+ }
+ }
+
+ private synchronized void persist(PermissionState state) {
+ if (!getPrefs().edit()
+ .putStringSet(state.getKey(), state.toPersistable())
+ .putBoolean(state.getKey() + ALL_SUFFIX, state.hasAllPermissions())
+ .commit()) {
+ Log.e(TAG, "Unable to persist permissions");
+ }
+ }
+
+ private PermissionState getPermissionState(String pkg, String authority) {
+ String key = pkg + "_" + authority;
+ Set<String> grant = getPrefs().getStringSet(key, Collections.<String>emptySet());
+ boolean hasAllPermissions = getPrefs().getBoolean(key + ALL_SUFFIX, false);
+ return new PermissionState(grant, key, hasAllPermissions);
+ }
+
+ public static class PermissionState {
+
+ private final ArraySet<String[]> mPaths = new ArraySet<>();
+ private final String mKey;
+
+ PermissionState(Set<String> grant, String key, boolean hasAllPermissions) {
+ if (hasAllPermissions) {
+ mPaths.add(new String[0]);
+ } else {
+ for (String g : grant) {
+ mPaths.add(decodeSegments(g));
+ }
+ }
+ mKey = key;
+ }
+
+ public boolean hasAllPermissions() {
+ return hasAccess(Collections.<String>emptyList());
+ }
+
+ public String getKey() {
+ return mKey;
+ }
+
+ public Set<String> toPersistable() {
+ ArraySet<String> ret = new ArraySet<>();
+ for (String[] path : mPaths) {
+ ret.add(encodeSegments(path));
+ }
+ return ret;
+ }
+
+ public boolean hasAccess(List<String> path) {
+ String[] inPath = path.toArray(new String[path.size()]);
+ for (String[] p : mPaths) {
+ if (isPathPrefixMatch(p, inPath)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ boolean addPath(List<String> path) {
+ String[] pathSegs = path.toArray(new String[path.size()]);
+ for (int i = mPaths.size() - 1; i >= 0; i--) {
+ String[] existing = mPaths.valueAt(i);
+ if (isPathPrefixMatch(existing, pathSegs)) {
+ // Nothing to add here.
+ return false;
+ }
+ if (isPathPrefixMatch(pathSegs, existing)) {
+ mPaths.removeAt(i);
+ }
+ }
+ mPaths.add(pathSegs);
+ return true;
+ }
+
+ boolean removePath(List<String> path) {
+ boolean changed = false;
+ String[] pathSegs = path.toArray(new String[path.size()]);
+ for (int i = mPaths.size() - 1; i >= 0; i--) {
+ String[] existing = mPaths.valueAt(i);
+ if (isPathPrefixMatch(pathSegs, existing)) {
+ changed = true;
+ mPaths.removeAt(i);
+ }
+ }
+ return changed;
+ }
+
+ private boolean isPathPrefixMatch(String[] prefix, String[] path) {
+ final int prefixSize = prefix.length;
+ if (path.length < prefixSize) return false;
+
+ for (int i = 0; i < prefixSize; i++) {
+ if (!Objects.equals(path[i], prefix[i])) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private String encodeSegments(String[] s) {
+ String[] out = new String[s.length];
+ for (int i = 0; i < s.length; i++) {
+ out[i] = Uri.encode(s[i]);
+ }
+ return TextUtils.join("/", out);
+ }
+
+ private String[] decodeSegments(String s) {
+ String[] sets = s.split("/", -1);
+ for (int i = 0; i < sets.length; i++) {
+ sets[i] = Uri.decode(sets[i]);
+ }
+ return sets;
+ }
+ }
+}
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 f3270f8..5feaa1e 100644
--- a/slices/core/src/main/java/androidx/slice/compat/SlicePermissionActivity.java
+++ b/slices/core/src/main/java/androidx/slice/compat/SlicePermissionActivity.java
@@ -20,7 +20,6 @@
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.DialogInterface.OnDismissListener;
-import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.net.Uri;
@@ -78,11 +77,8 @@
@Override
public void onClick(DialogInterface dialog, int which) {
if (which == DialogInterface.BUTTON_POSITIVE) {
- grantUriPermission(mCallingPkg, mUri.buildUpon().path("").build(),
- Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
- | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
- | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
- getContentResolver().notifyChange(mUri, null);
+ SliceProviderCompat.grantSlicePermission(this, getPackageName(), mCallingPkg,
+ mUri.buildUpon().path("").build());
}
finish();
}
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 61bc65e..2d06d8c 100644
--- a/slices/core/src/main/java/androidx/slice/compat/SliceProviderCompat.java
+++ b/slices/core/src/main/java/androidx/slice/compat/SliceProviderCompat.java
@@ -15,8 +15,12 @@
*/
package androidx.slice.compat;
+import static android.app.slice.SliceManager.CATEGORY_SLICE;
+import static android.app.slice.SliceManager.SLICE_METADATA_KEY;
import static android.app.slice.SliceProvider.SLICE_TYPE;
+import static androidx.core.content.PermissionChecker.PERMISSION_DENIED;
+
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.Context;
@@ -24,10 +28,15 @@
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
+import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
import android.os.Parcelable;
+import android.os.Process;
import android.os.RemoteException;
+import android.os.StrictMode;
import android.util.Log;
import androidx.annotation.NonNull;
@@ -36,8 +45,8 @@
import androidx.collection.ArraySet;
import androidx.core.util.Preconditions;
import androidx.slice.Slice;
+import androidx.slice.SliceProvider;
import androidx.slice.SliceSpec;
-import androidx.slice.core.SliceHints;
import java.util.ArrayList;
import java.util.Collection;
@@ -51,8 +60,11 @@
@RestrictTo(Scope.LIBRARY)
public class SliceProviderCompat {
private static final String TAG = "SliceProviderCompat";
+ private static final String DATA_PREFIX = "slice_data_";
+ private static final String PERMS_PREFIX = "slice_perms_";
- public static final String EXTRA_BIND_URI = "slice_uri";
+ private static final long SLICE_BIND_ANR = 2000;
+
public static final String METHOD_SLICE = "bind_slice";
public static final String METHOD_MAP_INTENT = "map_slice";
public static final String METHOD_PIN = "pin_slice";
@@ -60,7 +72,11 @@
public static final String METHOD_GET_PINNED_SPECS = "get_specs";
public static final String METHOD_MAP_ONLY_INTENT = "map_only";
public static final String METHOD_GET_DESCENDANTS = "get_descendants";
+ public static final String METHOD_CHECK_PERMISSION = "check_perms";
+ public static final String METHOD_GRANT_PERMISSION = "grant_perms";
+ public static final String METHOD_REVOKE_PERMISSION = "revoke_perms";
+ public static final String EXTRA_BIND_URI = "slice_uri";
public static final String EXTRA_INTENT = "slice_intent";
public static final String EXTRA_SLICE = "slice";
public static final String EXTRA_SUPPORTED_SPECS = "specs";
@@ -68,6 +84,198 @@
public static final String EXTRA_PKG = "pkg";
public static final String EXTRA_PROVIDER_PKG = "provider_pkg";
public static final String EXTRA_SLICE_DESCENDANTS = "slice_descendants";
+ public static final String EXTRA_UID = "uid";
+ public static final String EXTRA_PID = "pid";
+ public static final String EXTRA_RESULT = "result";
+
+ private final Handler mHandler = new Handler(Looper.getMainLooper());
+
+ private String mCallback;
+ private final SliceProvider mProvider;
+ private CompatPinnedList mPinnedList;
+ private CompatPermissionManager mPermissionManager;
+
+ public SliceProviderCompat(SliceProvider provider) {
+ mProvider = provider;
+ mPinnedList = new CompatPinnedList(provider.getContext(),
+ DATA_PREFIX + getClass().getName());
+ mPermissionManager = new CompatPermissionManager(provider.getContext(),
+ PERMS_PREFIX + getClass().getName());
+ }
+
+ private Context getContext() {
+ return mProvider.getContext();
+ }
+
+ private String getCallingPackage() {
+ return mProvider.getCallingPackage();
+ }
+
+ /**
+ * Called by SliceProvider when compat is needed.
+ */
+ 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());
+ Bundle b = new Bundle();
+ b.putParcelable(EXTRA_SLICE, s.toBundle());
+ return b;
+ } else if (method.equals(METHOD_MAP_INTENT)) {
+ Intent intent = extras.getParcelable(EXTRA_INTENT);
+ Uri uri = mProvider.onMapIntentToUri(intent);
+ Bundle b = new Bundle();
+ if (uri != null) {
+ Set<SliceSpec> specs = getSpecs(extras);
+ Slice s = handleBindSlice(uri, specs, getCallingPackage());
+ b.putParcelable(EXTRA_SLICE, s.toBundle());
+ } else {
+ b.putParcelable(EXTRA_SLICE, null);
+ }
+ return b;
+ } else if (method.equals(METHOD_MAP_ONLY_INTENT)) {
+ Intent intent = extras.getParcelable(EXTRA_INTENT);
+ Uri uri = mProvider.onMapIntentToUri(intent);
+ Bundle b = new Bundle();
+ b.putParcelable(EXTRA_SLICE, uri);
+ return b;
+ } else if (method.equals(METHOD_PIN)) {
+ Uri uri = extras.getParcelable(EXTRA_BIND_URI);
+ Set<SliceSpec> specs = getSpecs(extras);
+ String pkg = extras.getString(EXTRA_PKG);
+ if (mPinnedList.addPin(uri, pkg, specs)) {
+ handleSlicePinned(uri);
+ }
+ return null;
+ } else if (method.equals(METHOD_UNPIN)) {
+ Uri uri = extras.getParcelable(EXTRA_BIND_URI);
+ String pkg = extras.getString(EXTRA_PKG);
+ if (mPinnedList.removePin(uri, pkg)) {
+ handleSliceUnpinned(uri);
+ }
+ return null;
+ } else if (method.equals(METHOD_GET_PINNED_SPECS)) {
+ Uri uri = extras.getParcelable(EXTRA_BIND_URI);
+ Bundle b = new Bundle();
+ addSpecs(b, mPinnedList.getSpecs(uri));
+ return b;
+ } else if (method.equals(METHOD_GET_DESCENDANTS)) {
+ Uri uri = extras.getParcelable(EXTRA_BIND_URI);
+ Bundle b = new Bundle();
+ b.putParcelableArrayList(EXTRA_SLICE_DESCENDANTS,
+ new ArrayList<>(handleGetDescendants(uri)));
+ return b;
+ } else if (method.equals(METHOD_CHECK_PERMISSION)) {
+ Uri uri = extras.getParcelable(EXTRA_BIND_URI);
+ String pkg = extras.getString(EXTRA_PKG);
+ int pid = extras.getInt(EXTRA_PID);
+ int uid = extras.getInt(EXTRA_UID);
+ Bundle b = new Bundle();
+ b.putInt(EXTRA_RESULT, mPermissionManager.checkSlicePermission(uri, pid, uid));
+ return b;
+ } else if (method.equals(METHOD_GRANT_PERMISSION)) {
+ Uri uri = extras.getParcelable(EXTRA_BIND_URI);
+ String toPkg = extras.getString(EXTRA_PKG);
+ if (Binder.getCallingUid() != Process.myUid()) {
+ throw new SecurityException("Only the owning process can manage slice permissions");
+ }
+ mPermissionManager.grantSlicePermission(uri, toPkg);
+ } else if (method.equals(METHOD_REVOKE_PERMISSION)) {
+ Uri uri = extras.getParcelable(EXTRA_BIND_URI);
+ String toPkg = extras.getString(EXTRA_PKG);
+ if (Binder.getCallingUid() != Process.myUid()) {
+ throw new SecurityException("Only the owning process can manage slice permissions");
+ }
+ mPermissionManager.revokeSlicePermission(uri, toPkg);
+ }
+ return null;
+ }
+
+ private Collection<Uri> handleGetDescendants(Uri uri) {
+ mCallback = "onGetSliceDescendants";
+ mHandler.postDelayed(mAnr, SLICE_BIND_ANR);
+ try {
+ return mProvider.onGetSliceDescendants(uri);
+ } finally {
+ mHandler.removeCallbacks(mAnr);
+ }
+ }
+
+ private void handleSlicePinned(final Uri sliceUri) {
+ mCallback = "onSlicePinned";
+ mHandler.postDelayed(mAnr, SLICE_BIND_ANR);
+ try {
+ mProvider.onSlicePinned(sliceUri);
+ } finally {
+ mHandler.removeCallbacks(mAnr);
+ }
+ }
+
+ private void handleSliceUnpinned(final Uri sliceUri) {
+ mCallback = "onSliceUnpinned";
+ mHandler.postDelayed(mAnr, SLICE_BIND_ANR);
+ try {
+ mProvider.onSliceUnpinned(sliceUri);
+ } finally {
+ mHandler.removeCallbacks(mAnr);
+ }
+ }
+
+ private Slice handleBindSlice(final Uri sliceUri, final Set<SliceSpec> specs,
+ final String callingPkg) {
+ // This can be removed once Slice#bindSlice is removed and everyone is using
+ // SliceManager#bindSlice.
+ String pkg = callingPkg != null ? callingPkg
+ : getContext().getPackageManager().getNameForUid(Binder.getCallingUid());
+ if (Binder.getCallingUid() != Process.myUid()) {
+ try {
+ getContext().enforceUriPermission(sliceUri,
+ Binder.getCallingPid(), Binder.getCallingUid(),
+ Intent.FLAG_GRANT_WRITE_URI_PERMISSION,
+ "Slice binding requires write access to Uri");
+ } catch (SecurityException e) {
+ return mProvider.createPermissionSlice(getContext(), sliceUri, pkg);
+ }
+ }
+ return onBindSliceStrict(sliceUri, specs);
+ }
+
+ private Slice onBindSliceStrict(Uri sliceUri, Set<SliceSpec> specs) {
+ StrictMode.ThreadPolicy oldPolicy = StrictMode.getThreadPolicy();
+ mCallback = "onBindSlice";
+ mHandler.postDelayed(mAnr, SLICE_BIND_ANR);
+ try {
+ StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
+ .detectAll()
+ .penaltyDeath()
+ .build());
+ SliceProvider.setSpecs(specs);
+ try {
+ return mProvider.onBindSlice(sliceUri);
+ } finally {
+ SliceProvider.setSpecs(null);
+ mHandler.removeCallbacks(mAnr);
+ }
+ } finally {
+ StrictMode.setThreadPolicy(oldPolicy);
+ }
+ }
+
+ private final Runnable mAnr = new Runnable() {
+ @Override
+ public void run() {
+ Process.sendSignal(Process.myPid(), Process.SIGNAL_QUIT);
+ Log.wtf(TAG, "Timed out while handling slice callback " + mCallback);
+ }
+ };
/**
* Compat version of {@link Slice#bindSlice}.
@@ -137,6 +345,10 @@
*/
public static Slice bindSlice(Context context, Intent intent,
Set<SliceSpec> supportedSpecs) {
+ Preconditions.checkNotNull(intent, "intent");
+ Preconditions.checkArgument(intent.getComponent() != null || intent.getPackage() != null
+ || intent.getData() != null,
+ String.format("Slice intent must be explicit %s", intent));
ContentResolver resolver = context.getContentResolver();
// Check if the intent has data for the slice uri on it and use that
@@ -145,10 +357,24 @@
return bindSlice(context, intentData, supportedSpecs);
}
// Otherwise ask the app
+ Intent queryIntent = new Intent(intent);
+ if (!queryIntent.hasCategory(CATEGORY_SLICE)) {
+ queryIntent.addCategory(CATEGORY_SLICE);
+ }
List<ResolveInfo> providers =
- context.getPackageManager().queryIntentContentProviders(intent, 0);
- if (providers == null) {
- throw new IllegalArgumentException("Unable to resolve intent " + intent);
+ context.getPackageManager().queryIntentContentProviders(queryIntent, 0);
+ if (providers == null || providers.isEmpty()) {
+ // There are no providers, see if this activity has a direct link.
+ ResolveInfo resolve = context.getPackageManager().resolveActivity(intent,
+ PackageManager.GET_META_DATA);
+ if (resolve != null && resolve.activityInfo != null
+ && resolve.activityInfo.metaData != null
+ && resolve.activityInfo.metaData.containsKey(SLICE_METADATA_KEY)) {
+ return bindSlice(context, Uri.parse(
+ resolve.activityInfo.metaData.getString(SLICE_METADATA_KEY)),
+ supportedSpecs);
+ }
+ return null;
}
String authority = providers.get(0).providerInfo.authority;
Uri uri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
@@ -258,7 +484,8 @@
*/
public static Uri mapIntentToUri(Context context, Intent intent) {
Preconditions.checkNotNull(intent, "intent");
- Preconditions.checkArgument(intent.getComponent() != null || intent.getPackage() != null,
+ Preconditions.checkArgument(intent.getComponent() != null || intent.getPackage() != null
+ || intent.getData() != null,
String.format("Slice intent must be explicit %s", intent));
ContentResolver resolver = context.getContentResolver();
@@ -268,17 +495,21 @@
return intentData;
}
// Otherwise ask the app
+ Intent queryIntent = new Intent(intent);
+ if (!queryIntent.hasCategory(CATEGORY_SLICE)) {
+ queryIntent.addCategory(CATEGORY_SLICE);
+ }
List<ResolveInfo> providers =
- context.getPackageManager().queryIntentContentProviders(intent, 0);
+ context.getPackageManager().queryIntentContentProviders(queryIntent, 0);
if (providers == null || providers.isEmpty()) {
// There are no providers, see if this activity has a direct link.
ResolveInfo resolve = context.getPackageManager().resolveActivity(intent,
PackageManager.GET_META_DATA);
if (resolve != null && resolve.activityInfo != null
&& resolve.activityInfo.metaData != null
- && resolve.activityInfo.metaData.containsKey(SliceHints.SLICE_METADATA_KEY)) {
+ && resolve.activityInfo.metaData.containsKey(SLICE_METADATA_KEY)) {
return Uri.parse(
- resolve.activityInfo.metaData.getString(SliceHints.SLICE_METADATA_KEY));
+ resolve.activityInfo.metaData.getString(SLICE_METADATA_KEY));
}
return null;
}
@@ -319,6 +550,60 @@
return Collections.emptyList();
}
- private SliceProviderCompat() {
+ /**
+ * Compat version of {@link android.app.slice.SliceManager#checkSlicePermission}.
+ */
+ public static int checkSlicePermission(Context context, String packageName, Uri uri, int pid,
+ int uid) {
+ ContentResolver resolver = context.getContentResolver();
+ try (ContentProviderClient provider = resolver.acquireContentProviderClient(uri)) {
+ Bundle extras = new Bundle();
+ extras.putParcelable(EXTRA_BIND_URI, uri);
+ extras.putString(EXTRA_PKG, packageName);
+ extras.putInt(EXTRA_PID, pid);
+ extras.putInt(EXTRA_UID, uid);
+
+ final Bundle res = provider.call(METHOD_CHECK_PERMISSION, null, extras);
+ return res.getInt(EXTRA_RESULT);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Unable to check slice permission", e);
+ }
+ return PERMISSION_DENIED;
+ }
+
+ /**
+ * Compat version of {@link android.app.slice.SliceManager#grantSlicePermission}.
+ */
+ public static void grantSlicePermission(Context context, String packageName, String toPackage,
+ Uri uri) {
+ ContentResolver resolver = context.getContentResolver();
+ try (ContentProviderClient provider = resolver.acquireContentProviderClient(uri)) {
+ Bundle extras = new Bundle();
+ extras.putParcelable(EXTRA_BIND_URI, uri);
+ extras.putString(EXTRA_PROVIDER_PKG, packageName);
+ extras.putString(EXTRA_PKG, toPackage);
+
+ provider.call(METHOD_GRANT_PERMISSION, null, extras);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Unable to get slice descendants", e);
+ }
+ }
+
+ /**
+ * Compat version of {@link android.app.slice.SliceManager#revokeSlicePermission}.
+ */
+ public static void revokeSlicePermission(Context context, String packageName, String toPackage,
+ Uri uri) {
+ ContentResolver resolver = context.getContentResolver();
+ try (ContentProviderClient provider = resolver.acquireContentProviderClient(uri)) {
+ Bundle extras = new Bundle();
+ extras.putParcelable(EXTRA_BIND_URI, uri);
+ extras.putString(EXTRA_PROVIDER_PKG, packageName);
+ extras.putString(EXTRA_PKG, toPackage);
+
+ provider.call(METHOD_REVOKE_PERMISSION, null, extras);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Unable to get slice descendants", e);
+ }
}
}
diff --git a/slices/view/api/current.txt b/slices/view/api/current.txt
index 4d97ef2..5a37826 100644
--- a/slices/view/api/current.txt
+++ b/slices/view/api/current.txt
@@ -113,6 +113,7 @@
ctor public SliceView(android.content.Context, android.util.AttributeSet, int);
ctor public SliceView(android.content.Context, android.util.AttributeSet, int, int);
method public int getMode();
+ method public androidx.slice.Slice getSlice();
method public java.util.List<androidx.slice.SliceItem> getSliceActions();
method public void onChanged(androidx.slice.Slice);
method public void onClick(android.view.View);
diff --git a/slices/view/build.gradle b/slices/view/build.gradle
index 5836d20..d214a19 100644
--- a/slices/view/build.gradle
+++ b/slices/view/build.gradle
@@ -23,8 +23,8 @@
}
dependencies {
- implementation(project(":slices-core"))
- implementation(project(":slices-builders"))
+ implementation(project(":slice-core"))
+ implementation(project(":slice-builders"))
implementation(project(":recyclerview"))
api(ARCH_LIFECYCLE_LIVEDATA_CORE, libs.exclude_annotations_transitive)
diff --git a/slices/view/src/androidTest/AndroidManifest.xml b/slices/view/src/androidTest/AndroidManifest.xml
index 78f3ad8..7c90f90 100644
--- a/slices/view/src/androidTest/AndroidManifest.xml
+++ b/slices/view/src/androidTest/AndroidManifest.xml
@@ -27,6 +27,7 @@
android:exported="true">
<intent-filter>
<action android:name="androidx.slice.action.TEST" />
+ <category android:name="android.app.slice.category.SLICE" />
</intent-filter>
</provider>
diff --git a/slices/view/src/androidTest/java/androidx/slice/SliceManagerTest.java b/slices/view/src/androidTest/java/androidx/slice/SliceManagerTest.java
index 61d5c2b..b54335d 100644
--- a/slices/view/src/androidTest/java/androidx/slice/SliceManagerTest.java
+++ b/slices/view/src/androidTest/java/androidx/slice/SliceManagerTest.java
@@ -145,8 +145,8 @@
when(mSliceProvider.onMapIntentToUri(eq(intent))).thenReturn(expected);
Uri uri = mManager.mapIntentToUri(intent);
- assertEquals(expected, uri);
verify(mSliceProvider).onMapIntentToUri(eq(intent));
+ assertEquals(expected, uri);
}
@Test
diff --git a/slices/view/src/androidTest/java/androidx/slice/SlicePermissionTest.java b/slices/view/src/androidTest/java/androidx/slice/SlicePermissionTest.java
new file mode 100644
index 0000000..ffb65e0
--- /dev/null
+++ b/slices/view/src/androidTest/java/androidx/slice/SlicePermissionTest.java
@@ -0,0 +1,185 @@
+/*
+ * 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.slice;
+
+import static androidx.core.content.PermissionChecker.PERMISSION_DENIED;
+import static androidx.core.content.PermissionChecker.PERMISSION_GRANTED;
+
+import static org.junit.Assert.assertEquals;
+
+import android.content.Context;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.net.Uri;
+import android.os.Process;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class SlicePermissionTest {
+
+ private static final Uri BASE_URI = Uri.parse("content://androidx.slice.view.test/");
+ private final Context mContext = InstrumentationRegistry.getContext();
+ private String mTestPkg;
+ private int mTestUid;
+ private int mTestPid;
+ private SliceManager mSliceManager;
+
+ @Before
+ public void setup() throws NameNotFoundException {
+ mSliceManager = SliceManager.getInstance(mContext);
+ mTestPkg = mContext.getPackageName();
+ mTestUid = mContext.getPackageManager().getPackageUid(mTestPkg, 0);
+ mTestPid = Process.myPid();
+ }
+
+ @After
+ public void tearDown() {
+ mSliceManager.revokeSlicePermission(mTestPkg, BASE_URI);
+ }
+
+ @Test
+ public void testGrant() {
+ assertEquals(PERMISSION_DENIED,
+ mSliceManager.checkSlicePermission(BASE_URI, mTestPid, mTestUid));
+
+ mSliceManager.grantSlicePermission(mTestPkg, BASE_URI);
+
+ assertEquals(PERMISSION_GRANTED,
+ mSliceManager.checkSlicePermission(BASE_URI, mTestPid, mTestUid));
+ }
+
+ @Test
+ public void testGrantParent() {
+ Uri uri = BASE_URI.buildUpon()
+ .appendPath("something")
+ .build();
+
+ assertEquals(PERMISSION_DENIED,
+ mSliceManager.checkSlicePermission(uri, mTestPid, mTestUid));
+
+ mSliceManager.grantSlicePermission(mTestPkg, BASE_URI);
+
+ assertEquals(PERMISSION_GRANTED,
+ mSliceManager.checkSlicePermission(uri, mTestPid, mTestUid));
+ }
+
+ @Test
+ public void testGrantParentExpands() {
+ Uri uri = BASE_URI.buildUpon()
+ .appendPath("something")
+ .build();
+
+ assertEquals(PERMISSION_DENIED,
+ mSliceManager.checkSlicePermission(uri, mTestPid, mTestUid));
+
+ mSliceManager.grantSlicePermission(mTestPkg, uri);
+
+ // Only sub-path granted.
+ assertEquals(PERMISSION_GRANTED,
+ mSliceManager.checkSlicePermission(uri, mTestPid, mTestUid));
+ assertEquals(PERMISSION_DENIED,
+ mSliceManager.checkSlicePermission(BASE_URI, mTestPid, mTestUid));
+
+ mSliceManager.grantSlicePermission(mTestPkg, BASE_URI);
+
+ // Now all granted.
+ assertEquals(PERMISSION_GRANTED,
+ mSliceManager.checkSlicePermission(uri, mTestPid, mTestUid));
+ assertEquals(PERMISSION_GRANTED,
+ mSliceManager.checkSlicePermission(BASE_URI, mTestPid, mTestUid));
+ }
+
+ @Test
+ public void testGrantChild() {
+ Uri uri = BASE_URI.buildUpon()
+ .appendPath("something")
+ .build();
+
+ assertEquals(PERMISSION_DENIED,
+ mSliceManager.checkSlicePermission(BASE_URI, mTestPid, mTestUid));
+
+ mSliceManager.grantSlicePermission(mTestPkg, uri);
+
+ // Still no permission because only a child was granted
+ assertEquals(PERMISSION_DENIED,
+ mSliceManager.checkSlicePermission(BASE_URI, mTestPid, mTestUid));
+ }
+
+ @Test
+ public void testRevoke() {
+ assertEquals(PERMISSION_DENIED,
+ mSliceManager.checkSlicePermission(BASE_URI, mTestPid, mTestUid));
+
+ mSliceManager.grantSlicePermission(mTestPkg, BASE_URI);
+
+ assertEquals(PERMISSION_GRANTED,
+ mSliceManager.checkSlicePermission(BASE_URI, mTestPid, mTestUid));
+
+ mSliceManager.revokeSlicePermission(mTestPkg, BASE_URI);
+
+ assertEquals(PERMISSION_DENIED,
+ mSliceManager.checkSlicePermission(BASE_URI, mTestPid, mTestUid));
+ }
+
+ @Test
+ public void testRevokeParent() {
+ Uri uri = BASE_URI.buildUpon()
+ .appendPath("something")
+ .build();
+ assertEquals(PERMISSION_DENIED,
+ mSliceManager.checkSlicePermission(uri, mTestPid, mTestUid));
+
+ mSliceManager.grantSlicePermission(mTestPkg, uri);
+
+ assertEquals(PERMISSION_GRANTED,
+ mSliceManager.checkSlicePermission(uri, mTestPid, mTestUid));
+
+ mSliceManager.revokeSlicePermission(mTestPkg, BASE_URI);
+
+ // Revoked because parent was revoked
+ assertEquals(PERMISSION_DENIED,
+ mSliceManager.checkSlicePermission(uri, mTestPid, mTestUid));
+ }
+
+ @Test
+ public void testRevokeChild() {
+ Uri uri = BASE_URI.buildUpon()
+ .appendPath("something")
+ .build();
+ assertEquals(PERMISSION_DENIED,
+ mSliceManager.checkSlicePermission(BASE_URI, mTestPid, mTestUid));
+
+ mSliceManager.grantSlicePermission(mTestPkg, BASE_URI);
+
+ assertEquals(PERMISSION_GRANTED,
+ mSliceManager.checkSlicePermission(BASE_URI, mTestPid, mTestUid));
+
+ mSliceManager.revokeSlicePermission(mTestPkg, uri);
+
+ // Not revoked because child was revoked.
+ assertEquals(PERMISSION_GRANTED,
+ mSliceManager.checkSlicePermission(BASE_URI, mTestPid, mTestUid));
+ }
+
+}
diff --git a/slices/view/src/main/java/androidx/slice/SliceManager.java b/slices/view/src/main/java/androidx/slice/SliceManager.java
index 63c56e8..37a1960 100644
--- a/slices/view/src/main/java/androidx/slice/SliceManager.java
+++ b/slices/view/src/main/java/androidx/slice/SliceManager.java
@@ -141,9 +141,8 @@
public abstract @Nullable Slice bindSlice(@NonNull Uri uri);
/**
- * Turns a slice intent into slice content. Expects an explicit intent. If there is no
- * {@link android.content.ContentProvider} associated with the given intent this will throw
- * {@link IllegalArgumentException}.
+ * Turns a slice intent into slice content. Is a shortcut to perform the action
+ * of both {@link #mapIntentToUri(Intent)} and {@link #bindSlice(Uri)} at once.
*
* @param intent The intent associated with a slice.
* @return The Slice provided by the app or null if none is given.
@@ -154,12 +153,23 @@
public abstract @Nullable Slice bindSlice(@NonNull Intent intent);
/**
- * Turns a slice intent into a slice uri. Expects an explicit intent. If there is no
- * {@link android.content.ContentProvider} associated with the given intent this will throw
- * {@link IllegalArgumentException}.
- *
+ * Turns a slice intent into a slice uri. Expects an explicit intent.
+ * <p>
+ * This goes through a several stage resolution process to determine if any slice
+ * can represent this intent.
+ * <ol>
+ * <li> If the intent contains data that {@link android.content.ContentResolver#getType} is
+ * {@link android.app.slice.SliceProvider#SLICE_TYPE} then the data will be returned.</li>
+ * <li>If the intent explicitly points at an activity, and that activity has
+ * meta-data for key {@link android.app.slice.SliceManager#SLICE_METADATA_KEY},
+ * then the Uri specified there will be returned.</li>
+ * <li>Lastly, if the intent with {@link android.app.slice.SliceManager#CATEGORY_SLICE} added
+ * resolves to a provider, then the provider will be asked to
+ * {@link SliceProvider#onMapIntentToUri} and that result will be returned.</li>
+ * <li>If no slice is found, then {@code null} is returned.</li>
+ * </ol>
* @param intent The intent associated with a slice.
- * @return The Slice Uri provided by the app or null if none is given.
+ * @return The Slice Uri provided by the app or null if none exists.
* @see Slice
* @see SliceProvider#onMapIntentToUri(Intent)
* @see Intent
diff --git a/slices/view/src/main/java/androidx/slice/SliceManagerBase.java b/slices/view/src/main/java/androidx/slice/SliceManagerBase.java
index fb9fccd..5192dad 100644
--- a/slices/view/src/main/java/androidx/slice/SliceManagerBase.java
+++ b/slices/view/src/main/java/androidx/slice/SliceManagerBase.java
@@ -19,11 +19,9 @@
import static androidx.slice.widget.SliceLiveData.SUPPORTED_SPECS;
import android.content.Context;
-import android.content.Intent;
import android.database.ContentObserver;
import android.net.Uri;
import android.os.AsyncTask;
-import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.util.ArrayMap;
@@ -31,7 +29,6 @@
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
-import androidx.core.content.PermissionChecker;
import java.util.concurrent.Executor;
@@ -71,31 +68,6 @@
if (impl != null) impl.stopListening();
}
- @Override
- @PermissionChecker.PermissionResult
- public int checkSlicePermission(@NonNull Uri uri, int pid, int uid) {
- // TODO: Switch off Uri permissions.
- return mContext.checkUriPermission(uri, pid, uid,
- Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
- }
-
- @Override
- public void grantSlicePermission(@NonNull String toPackage, @NonNull Uri uri) {
- // TODO: Switch off Uri permissions.
- mContext.grantUriPermission(toPackage, uri,
- Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
- | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
- | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
- }
-
- @Override
- public void revokeSlicePermission(@NonNull String toPackage, @NonNull Uri uri) {
- // TODO: Switch off Uri permissions.
- if (Build.VERSION.SDK_INT >= 26) {
- mContext.revokeUriPermission(toPackage, uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
- }
- }
-
private SliceListenerImpl getListener(Uri uri, SliceCallback callback,
SliceListenerImpl listener) {
diff --git a/slices/view/src/main/java/androidx/slice/SliceManagerCompat.java b/slices/view/src/main/java/androidx/slice/SliceManagerCompat.java
index 1badbb4..f8cda4d 100644
--- a/slices/view/src/main/java/androidx/slice/SliceManagerCompat.java
+++ b/slices/view/src/main/java/androidx/slice/SliceManagerCompat.java
@@ -76,6 +76,24 @@
}
@Override
+ public int checkSlicePermission(Uri uri, int pid, int uid) {
+ return SliceProviderCompat.checkSlicePermission(mContext, mContext.getPackageName(), uri,
+ pid, uid);
+ }
+
+ @Override
+ public void grantSlicePermission(String toPackage, Uri uri) {
+ SliceProviderCompat.grantSlicePermission(mContext, mContext.getPackageName(), toPackage,
+ uri);
+ }
+
+ @Override
+ public void revokeSlicePermission(String toPackage, Uri uri) {
+ SliceProviderCompat.revokeSlicePermission(mContext, mContext.getPackageName(), toPackage,
+ uri);
+ }
+
+ @Override
public Collection<Uri> getSliceDescendants(Uri uri) {
return SliceProviderCompat.getSliceDescendants(mContext, uri);
}
diff --git a/slices/view/src/main/java/androidx/slice/SliceManagerWrapper.java b/slices/view/src/main/java/androidx/slice/SliceManagerWrapper.java
index b28efd1..4c19b84 100644
--- a/slices/view/src/main/java/androidx/slice/SliceManagerWrapper.java
+++ b/slices/view/src/main/java/androidx/slice/SliceManagerWrapper.java
@@ -28,6 +28,7 @@
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
+import androidx.core.content.PermissionChecker;
import java.util.ArrayList;
import java.util.Collection;
@@ -89,6 +90,22 @@
return mManager.getSliceDescendants(uri);
}
+ @Override
+ @PermissionChecker.PermissionResult
+ public int checkSlicePermission(@NonNull Uri uri, int pid, int uid) {
+ return mManager.checkSlicePermission(uri, pid, uid);
+ }
+
+ @Override
+ public void grantSlicePermission(@NonNull String toPackage, @NonNull Uri uri) {
+ mManager.grantSlicePermission(toPackage, uri);
+ }
+
+ @Override
+ public void revokeSlicePermission(@NonNull String toPackage, @NonNull Uri uri) {
+ mManager.revokeSlicePermission(toPackage, uri);
+ }
+
@Nullable
@Override
public Uri mapIntentToUri(@NonNull Intent intent) {
diff --git a/slices/view/src/main/java/androidx/slice/widget/ListContent.java b/slices/view/src/main/java/androidx/slice/widget/ListContent.java
index cde20da..1cbf357 100644
--- a/slices/view/src/main/java/androidx/slice/widget/ListContent.java
+++ b/slices/view/src/main/java/androidx/slice/widget/ListContent.java
@@ -108,9 +108,12 @@
* @return the total height of all the rows contained in the provided list.
*/
public static int getListHeight(Context context, List<SliceItem> listItems) {
+ if (listItems == null) {
+ return 0;
+ }
int height = 0;
boolean hasRealHeader = false;
- if (listItems.size() > 0) {
+ if (!listItems.isEmpty()) {
SliceItem maybeHeader = listItems.get(0);
hasRealHeader = !maybeHeader.hasAnyHints(HINT_LIST_ITEM, HINT_HORIZONTAL);
}
@@ -200,6 +203,7 @@
return mSeeMoreItem;
}
+ @NonNull
public ArrayList<SliceItem> getRowItems() {
return mRowItems;
}
diff --git a/slices/view/src/main/java/androidx/slice/widget/SliceView.java b/slices/view/src/main/java/androidx/slice/widget/SliceView.java
index 3eeda45..855bded 100644
--- a/slices/view/src/main/java/androidx/slice/widget/SliceView.java
+++ b/slices/view/src/main/java/androidx/slice/widget/SliceView.java
@@ -413,7 +413,18 @@
}
/**
+ * @return the slice being used to populate this view.
+ */
+ @Nullable
+ public Slice getSlice() {
+ return mCurrentSlice;
+ }
+
+ /**
* Returns the slice actions presented in this view.
+ * <p>
+ * Note that these may be different from {@link SliceMetadata#getSliceActions()} if the actions
+ * set on the view have been adjusted using {@link #setSliceActions(List)}.
*/
@Nullable
public List<SliceItem> getSliceActions() {
diff --git a/v7/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerTest.java b/v7/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerTest.java
index 25aa00d..60eb6d2 100644
--- a/v7/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerTest.java
+++ b/v7/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerTest.java
@@ -1180,7 +1180,16 @@
adapter.addAndNotify(5 + (i % 3) * 3, 1);
Thread.sleep(25);
}
- smoothScrollToPosition(mLayoutManager.findLastVisibleItemPosition() + 20);
+
+ final AtomicInteger lastVisiblePosition = new AtomicInteger();
+ mActivityRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ lastVisiblePosition.set(mLayoutManager.findLastVisibleItemPosition());
+ }
+ });
+
+ smoothScrollToPosition(lastVisiblePosition.get() + 20);
waitForAnimations(2);
getInstrumentation().waitForIdleSync();
assertEquals("Children count should add up", childCount.get(),
diff --git a/webkit/build.gradle b/webkit/build.gradle
index d610dad..baed9cb 100644
--- a/webkit/build.gradle
+++ b/webkit/build.gradle
@@ -40,7 +40,7 @@
}
buildTypes.all {
- consumerProguardFiles new File(webviewBoundaryInterfacesDir, "proguard.flags")
+ consumerProguardFiles new File(webviewBoundaryInterfacesDir, "proguard.flags") , 'proguard-rules.pro'
}
}
diff --git a/webkit/proguard-rules.pro b/webkit/proguard-rules.pro
new file mode 100644
index 0000000..86756ab
--- /dev/null
+++ b/webkit/proguard-rules.pro
@@ -0,0 +1,16 @@
+# 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.
+
+# Prevent WebViewClientCompat from being renamed, since chromium depends on this name.
+-keep public class androidx.webkit.WebViewClientCompat { public *; }
diff --git a/webkit/src/main/java/androidx/webkit/ServiceWorkerControllerCompat.java b/webkit/src/main/java/androidx/webkit/ServiceWorkerControllerCompat.java
index 8d9d683..79b714a 100644
--- a/webkit/src/main/java/androidx/webkit/ServiceWorkerControllerCompat.java
+++ b/webkit/src/main/java/androidx/webkit/ServiceWorkerControllerCompat.java
@@ -16,18 +16,11 @@
package androidx.webkit;
-import android.os.Build;
-import android.webkit.ServiceWorkerController;
-
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
import androidx.annotation.RequiresFeature;
import androidx.annotation.RestrictTo;
-import androidx.webkit.internal.FrameworkServiceWorkerController;
-import androidx.webkit.internal.ServiceWorkerControllerAdapter;
-import androidx.webkit.internal.WebViewFeatureInternal;
-import androidx.webkit.internal.WebViewGlueCommunicator;
+import androidx.webkit.internal.ServiceWorkerControllerImpl;
/**
* Manages Service Workers used by WebView.
@@ -68,34 +61,7 @@
}
private static class LAZY_HOLDER {
- static final ServiceWorkerControllerCompat INSTANCE = createController();
-
- @SuppressWarnings("NewApi")
- private static ServiceWorkerControllerCompat createController() {
- WebViewFeatureInternal webviewFeature =
- WebViewFeatureInternal.getFeature(WebViewFeature.SERVICE_WORKER_BASIC_USAGE);
- if (webviewFeature.isSupportedByFramework()) {
- return getFrameworkControllerCompat();
- } else if (webviewFeature.isSupportedByWebView()) {
- return getSupportLibraryControllerCompat();
- } else {
- throw WebViewFeatureInternal.getUnsupportedOperationException();
- }
- }
- }
-
- /**
- * Return a version of {@link ServiceWorkerControllerCompat} that only uses framework APIs.
- */
- @RequiresApi(Build.VERSION_CODES.N)
- private static ServiceWorkerControllerCompat getFrameworkControllerCompat() {
- return new FrameworkServiceWorkerController(
- ServiceWorkerController.getInstance());
- }
-
- private static ServiceWorkerControllerCompat getSupportLibraryControllerCompat() {
- return new ServiceWorkerControllerAdapter(
- WebViewGlueCommunicator.getFactory().getServiceWorkerController());
+ static final ServiceWorkerControllerCompat INSTANCE = new ServiceWorkerControllerImpl();
}
/**
diff --git a/webkit/src/main/java/androidx/webkit/internal/FrameworkServiceWorkerController.java b/webkit/src/main/java/androidx/webkit/internal/FrameworkServiceWorkerController.java
deleted file mode 100644
index 2e02777..0000000
--- a/webkit/src/main/java/androidx/webkit/internal/FrameworkServiceWorkerController.java
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * 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.os.Build;
-import android.webkit.ServiceWorkerController;
-
-import androidx.annotation.RequiresApi;
-import androidx.webkit.ServiceWorkerClientCompat;
-import androidx.webkit.ServiceWorkerControllerCompat;
-import androidx.webkit.ServiceWorkerWebSettingsCompat;
-
-/**
- * Implementation of {@link ServiceWorkerControllerCompat} meant for use on up-to-date platforms.
- * This class does not use reflection to bypass framework APIs - instead it uses android.webkit
- * APIs.
- */
-@RequiresApi(Build.VERSION_CODES.N)
-public class FrameworkServiceWorkerController extends ServiceWorkerControllerCompat {
- private final ServiceWorkerController mImpl;
- private ServiceWorkerWebSettingsCompat mSettings;
-
- public FrameworkServiceWorkerController(ServiceWorkerController impl) {
- mImpl = impl;
- }
-
- @Override
- public ServiceWorkerWebSettingsCompat getServiceWorkerWebSettings() {
- if (mSettings == null) {
- mSettings = new FrameworksServiceWorkerWebSettings(mImpl.getServiceWorkerWebSettings());
- }
- return mSettings;
- }
-
- @Override
- public void setServiceWorkerClient(ServiceWorkerClientCompat client) {
- mImpl.setServiceWorkerClient(new FrameworkServiceWorkerClient(client));
- }
-}
diff --git a/webkit/src/main/java/androidx/webkit/internal/FrameworksServiceWorkerWebSettings.java b/webkit/src/main/java/androidx/webkit/internal/FrameworksServiceWorkerWebSettings.java
deleted file mode 100644
index 4373756..0000000
--- a/webkit/src/main/java/androidx/webkit/internal/FrameworksServiceWorkerWebSettings.java
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * 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.os.Build;
-import android.webkit.ServiceWorkerWebSettings;
-
-import androidx.annotation.RequiresApi;
-import androidx.webkit.ServiceWorkerWebSettingsCompat;
-
-/**
- * Implementation of {@link ServiceWorkerWebSettingsCompat} meant for use on up-to-date platforms.
- * This class does not use reflection to bypass framework APIs - instead it uses android.webkit
- * APIs.
- */
-@RequiresApi(Build.VERSION_CODES.N)
-public class FrameworksServiceWorkerWebSettings extends ServiceWorkerWebSettingsCompat {
- private final ServiceWorkerWebSettings mImpl;
-
- public FrameworksServiceWorkerWebSettings(ServiceWorkerWebSettings impl) {
- mImpl = impl;
- }
-
- @Override
- public void setCacheMode(int mode) {
- mImpl.setCacheMode(mode);
- }
-
- @Override
- public int getCacheMode() {
- return mImpl.getCacheMode();
- }
-
- @Override
- public void setAllowContentAccess(boolean allow) {
- mImpl.setAllowContentAccess(allow);
- }
-
- @Override
- public boolean getAllowContentAccess() {
- return mImpl.getAllowContentAccess();
- }
-
- @Override
- public void setAllowFileAccess(boolean allow) {
- mImpl.setAllowContentAccess(allow);
- }
-
- @Override
- public boolean getAllowFileAccess() {
- return mImpl.getAllowFileAccess();
- }
-
- @Override
- public void setBlockNetworkLoads(boolean flag) {
- mImpl.setAllowContentAccess(flag);
- }
-
- @Override
- public boolean getBlockNetworkLoads() {
- return mImpl.getBlockNetworkLoads();
- }
-}
diff --git a/webkit/src/main/java/androidx/webkit/internal/ServiceWorkerControllerAdapter.java b/webkit/src/main/java/androidx/webkit/internal/ServiceWorkerControllerAdapter.java
deleted file mode 100644
index 4baa3ea..0000000
--- a/webkit/src/main/java/androidx/webkit/internal/ServiceWorkerControllerAdapter.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * 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 androidx.webkit.ServiceWorkerClientCompat;
-import androidx.webkit.ServiceWorkerControllerCompat;
-import androidx.webkit.ServiceWorkerWebSettingsCompat;
-
-import org.chromium.support_lib_boundary.ServiceWorkerControllerBoundaryInterface;
-import org.chromium.support_lib_boundary.ServiceWorkerWebSettingsBoundaryInterface;
-import org.chromium.support_lib_boundary.util.BoundaryInterfaceReflectionUtil;
-
-/**
- * Adapter between {@link ServiceWorkerControllerCompat} and
- * {@link ServiceWorkerControllerBoundaryInterface} (the corresponding interface shared with the
- * support library glue in the WebView APK).
- */
-public class ServiceWorkerControllerAdapter extends ServiceWorkerControllerCompat {
- private final ServiceWorkerControllerBoundaryInterface mImpl;
- private final ServiceWorkerWebSettingsCompat mWebSettings;
-
- public ServiceWorkerControllerAdapter(ServiceWorkerControllerBoundaryInterface impl) {
- mImpl = impl;
- mWebSettings = new ServiceWorkerWebSettingsAdapter(
- BoundaryInterfaceReflectionUtil.castToSuppLibClass(
- ServiceWorkerWebSettingsBoundaryInterface.class,
- mImpl.getServiceWorkerWebSettings()));
- }
-
- @Override
- public ServiceWorkerWebSettingsCompat getServiceWorkerWebSettings() {
- return mWebSettings;
- }
-
- @Override
- public void setServiceWorkerClient(ServiceWorkerClientCompat client) {
- mImpl.setServiceWorkerClient(BoundaryInterfaceReflectionUtil.createInvocationHandlerFor(
- new ServiceWorkerClientAdapter(client)));
- }
-}
diff --git a/webkit/src/main/java/androidx/webkit/internal/ServiceWorkerControllerImpl.java b/webkit/src/main/java/androidx/webkit/internal/ServiceWorkerControllerImpl.java
new file mode 100644
index 0000000..17e0a7d
--- /dev/null
+++ b/webkit/src/main/java/androidx/webkit/internal/ServiceWorkerControllerImpl.java
@@ -0,0 +1,90 @@
+/*
+ * 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.annotation.SuppressLint;
+import android.webkit.ServiceWorkerController;
+
+import androidx.webkit.ServiceWorkerClientCompat;
+import androidx.webkit.ServiceWorkerControllerCompat;
+import androidx.webkit.ServiceWorkerWebSettingsCompat;
+
+import org.chromium.support_lib_boundary.ServiceWorkerControllerBoundaryInterface;
+import org.chromium.support_lib_boundary.ServiceWorkerWebSettingsBoundaryInterface;
+import org.chromium.support_lib_boundary.util.BoundaryInterfaceReflectionUtil;
+
+/**
+ * Implementation of {@link ServiceWorkerControllerCompat}.
+ * This class uses either the framework, the WebView APK, or both, to implement
+ * {@link ServiceWorkerControllerCompat} functionality.
+ */
+public class ServiceWorkerControllerImpl extends ServiceWorkerControllerCompat {
+ private final ServiceWorkerController mFrameworksImpl;
+ private ServiceWorkerControllerBoundaryInterface mBoundaryInterface;
+ private final ServiceWorkerWebSettingsCompat mWebSettings;
+
+ @SuppressLint("NewApi")
+ public ServiceWorkerControllerImpl() {
+ final WebViewFeatureInternal feature = WebViewFeatureInternal.SERVICE_WORKER_BASIC_USAGE;
+ if (feature.isSupportedByFramework()) {
+ mFrameworksImpl = ServiceWorkerController.getInstance();
+ // The current WebView APK might not be compatible with the support library, so set the
+ // boundary interface to null for now.
+ mBoundaryInterface = null;
+ mWebSettings = new ServiceWorkerWebSettingsImpl(
+ mFrameworksImpl.getServiceWorkerWebSettings(), null);
+ } else if (feature.isSupportedByWebView()) {
+ mFrameworksImpl = null;
+ mBoundaryInterface = WebViewGlueCommunicator.getFactory().getServiceWorkerController();
+ mWebSettings = new ServiceWorkerWebSettingsImpl(null,
+ BoundaryInterfaceReflectionUtil.castToSuppLibClass(
+ ServiceWorkerWebSettingsBoundaryInterface.class,
+ mBoundaryInterface.getServiceWorkerWebSettings()));
+ } else {
+ throw WebViewFeatureInternal.getUnsupportedOperationException();
+ }
+ }
+
+ private ServiceWorkerControllerBoundaryInterface getBoundaryInterface() {
+ if (mBoundaryInterface != null) return mBoundaryInterface;
+
+ // If the boundary interface is null we must have a working frameworks implementation to
+ // convert into a boundary interface.
+ mBoundaryInterface = WebViewGlueCommunicator.getFactory().getServiceWorkerController();
+ return mBoundaryInterface;
+ }
+
+ @Override
+ public ServiceWorkerWebSettingsCompat getServiceWorkerWebSettings() {
+ return mWebSettings;
+ }
+
+ @SuppressLint("NewApi")
+ @Override
+ public void setServiceWorkerClient(ServiceWorkerClientCompat client) {
+ final WebViewFeatureInternal feature = WebViewFeatureInternal.SERVICE_WORKER_BASIC_USAGE;
+ if (feature.isSupportedByFramework()) {
+ mFrameworksImpl.setServiceWorkerClient(new FrameworkServiceWorkerClient(client));
+ } else if (feature.isSupportedByWebView()) {
+ getBoundaryInterface().setServiceWorkerClient(
+ BoundaryInterfaceReflectionUtil.createInvocationHandlerFor(
+ new ServiceWorkerClientAdapter(client)));
+ } else {
+ throw WebViewFeatureInternal.getUnsupportedOperationException();
+ }
+ }
+}
diff --git a/webkit/src/main/java/androidx/webkit/internal/ServiceWorkerWebSettingsAdapter.java b/webkit/src/main/java/androidx/webkit/internal/ServiceWorkerWebSettingsAdapter.java
deleted file mode 100644
index fd49396..0000000
--- a/webkit/src/main/java/androidx/webkit/internal/ServiceWorkerWebSettingsAdapter.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * 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 androidx.webkit.ServiceWorkerWebSettingsCompat;
-
-import org.chromium.support_lib_boundary.ServiceWorkerWebSettingsBoundaryInterface;
-
-/**
- * Adapter between {@link ServiceWorkerWebSettingsCompat} and
- * {@link ServiceWorkerWebSettingsBoundaryInterface} (the corresponding interface shared with the
- * support library glue in the WebView APK).
- */
-public class ServiceWorkerWebSettingsAdapter extends ServiceWorkerWebSettingsCompat {
- private final ServiceWorkerWebSettingsBoundaryInterface mImpl;
-
- public ServiceWorkerWebSettingsAdapter(ServiceWorkerWebSettingsBoundaryInterface impl) {
- mImpl = impl;
- }
-
- @Override
- public void setCacheMode(int mode) {
- mImpl.setCacheMode(mode);
- }
-
- @Override
- public int getCacheMode() {
- return mImpl.getCacheMode();
- }
-
- @Override
- public void setAllowContentAccess(boolean allow) {
- mImpl.setAllowContentAccess(allow);
- }
-
- @Override
- public boolean getAllowContentAccess() {
- return mImpl.getAllowContentAccess();
- }
-
- @Override
- public void setAllowFileAccess(boolean allow) {
- mImpl.setAllowFileAccess(allow);
- }
-
- @Override
- public boolean getAllowFileAccess() {
- return mImpl.getAllowFileAccess();
- }
-
- @Override
- public void setBlockNetworkLoads(boolean flag) {
- mImpl.setBlockNetworkLoads(flag);
- }
-
- @Override
- public boolean getBlockNetworkLoads() {
- return mImpl.getBlockNetworkLoads();
- }
-}
diff --git a/webkit/src/main/java/androidx/webkit/internal/ServiceWorkerWebSettingsImpl.java b/webkit/src/main/java/androidx/webkit/internal/ServiceWorkerWebSettingsImpl.java
new file mode 100644
index 0000000..9ac4450
--- /dev/null
+++ b/webkit/src/main/java/androidx/webkit/internal/ServiceWorkerWebSettingsImpl.java
@@ -0,0 +1,172 @@
+/*
+ * 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.annotation.SuppressLint;
+import android.webkit.ServiceWorkerWebSettings;
+
+import androidx.webkit.ServiceWorkerWebSettingsCompat;
+
+import org.chromium.support_lib_boundary.ServiceWorkerWebSettingsBoundaryInterface;
+
+/**
+ * Implementation of {@link ServiceWorkerWebSettingsCompat}.
+ * This class uses either the framework, the WebView APK, or both, to implement
+ * {@link ServiceWorkerWebSettingsCompat} functionality.
+ */
+public class ServiceWorkerWebSettingsImpl extends ServiceWorkerWebSettingsCompat {
+ private final ServiceWorkerWebSettings mFrameworksImpl;
+ private ServiceWorkerWebSettingsBoundaryInterface mBoundaryInterface;
+
+ /**
+ * This class handles three different scenarios:
+ * 1. The Android version on the device is high enough to support all APIs used.
+ * 2. The Android version on the device is too low to support any ServiceWorkerWebSettings APIs
+ * so we use the support library glue instead through
+ * {@link ServiceWorkerWebSettingsBoundaryInterface}.
+ * 3. The Android version on the device is high enough to support some ServiceWorkerWebSettings
+ * APIs, so we call into them using {@link android.webkit.ServiceWorkerWebSettings}, but the
+ * rest of the APIs are only supported by the support library glue, so whenever we call such an
+ * API we fetch a {@link ServiceWorkerWebSettingsBoundaryInterface} corresponding to our
+ * {@link android.webkit.ServiceWorkerWebSettings}.
+ */
+ public ServiceWorkerWebSettingsImpl(ServiceWorkerWebSettings frameworksImpl,
+ ServiceWorkerWebSettingsBoundaryInterface boundaryInterface) {
+ if (frameworksImpl == null && boundaryInterface == null) {
+ throw new IllegalArgumentException(
+ "Both of the possible implementations cannot be null!");
+ }
+ mFrameworksImpl = frameworksImpl;
+ mBoundaryInterface = boundaryInterface;
+ }
+
+ private ServiceWorkerWebSettingsBoundaryInterface getBoundaryInterface() {
+ if (mBoundaryInterface != null) return mBoundaryInterface;
+ // If the boundary interface is null we must have a working frameworks implementation to
+ // convert into a boundary interface.
+ mBoundaryInterface =
+ WebViewGlueCommunicator.getCompatConverter().convertServiceWorkerSettings(
+ mFrameworksImpl);
+ return mBoundaryInterface;
+ }
+
+ @SuppressLint("NewApi")
+ @Override
+ public void setCacheMode(int mode) {
+ final WebViewFeatureInternal feature = WebViewFeatureInternal.SERVICE_WORKER_CACHE_MODE;
+ if (feature.isSupportedByFramework()) {
+ mFrameworksImpl.setCacheMode(mode);
+ } else if (feature.isSupportedByWebView()) {
+ getBoundaryInterface().setCacheMode(mode);
+ } else {
+ throw WebViewFeatureInternal.getUnsupportedOperationException();
+ }
+ }
+
+ @SuppressLint("NewApi")
+ @Override
+ public int getCacheMode() {
+ final WebViewFeatureInternal feature = WebViewFeatureInternal.SERVICE_WORKER_CACHE_MODE;
+ if (feature.isSupportedByFramework()) {
+ return mFrameworksImpl.getCacheMode();
+ } else if (feature.isSupportedByWebView()) {
+ return getBoundaryInterface().getCacheMode();
+ } else {
+ throw WebViewFeatureInternal.getUnsupportedOperationException();
+ }
+ }
+
+ @SuppressLint("NewApi")
+ @Override
+ public void setAllowContentAccess(boolean allow) {
+ final WebViewFeatureInternal feature = WebViewFeatureInternal.SERVICE_WORKER_CONTENT_ACCESS;
+ if (feature.isSupportedByFramework()) {
+ mFrameworksImpl.setAllowContentAccess(allow);
+ } else if (feature.isSupportedByWebView()) {
+ getBoundaryInterface().setAllowContentAccess(allow);
+ } else {
+ throw WebViewFeatureInternal.getUnsupportedOperationException();
+ }
+ }
+
+ @SuppressLint("NewApi")
+ @Override
+ public boolean getAllowContentAccess() {
+ final WebViewFeatureInternal feature = WebViewFeatureInternal.SERVICE_WORKER_CONTENT_ACCESS;
+ if (feature.isSupportedByFramework()) {
+ return mFrameworksImpl.getAllowContentAccess();
+ } else if (feature.isSupportedByWebView()) {
+ return getBoundaryInterface().getAllowContentAccess();
+ } else {
+ throw WebViewFeatureInternal.getUnsupportedOperationException();
+ }
+ }
+
+ @SuppressLint("NewApi")
+ @Override
+ public void setAllowFileAccess(boolean allow) {
+ final WebViewFeatureInternal feature = WebViewFeatureInternal.SERVICE_WORKER_FILE_ACCESS;
+ if (feature.isSupportedByFramework()) {
+ mFrameworksImpl.setAllowFileAccess(allow);
+ } else if (feature.isSupportedByWebView()) {
+ getBoundaryInterface().setAllowFileAccess(allow);
+ } else {
+ throw WebViewFeatureInternal.getUnsupportedOperationException();
+ }
+ }
+
+ @SuppressLint("NewApi")
+ @Override
+ public boolean getAllowFileAccess() {
+ final WebViewFeatureInternal feature = WebViewFeatureInternal.SERVICE_WORKER_FILE_ACCESS;
+ if (feature.isSupportedByFramework()) {
+ return mFrameworksImpl.getAllowFileAccess();
+ } else if (feature.isSupportedByWebView()) {
+ return getBoundaryInterface().getAllowFileAccess();
+ } else {
+ throw WebViewFeatureInternal.getUnsupportedOperationException();
+ }
+ }
+
+ @SuppressLint("NewApi")
+ @Override
+ public void setBlockNetworkLoads(boolean flag) {
+ final WebViewFeatureInternal feature =
+ WebViewFeatureInternal.SERVICE_WORKER_BLOCK_NETWORK_LOADS;
+ if (feature.isSupportedByFramework()) {
+ mFrameworksImpl.setBlockNetworkLoads(flag);
+ } else if (feature.isSupportedByWebView()) {
+ getBoundaryInterface().setBlockNetworkLoads(flag);
+ } else {
+ throw WebViewFeatureInternal.getUnsupportedOperationException();
+ }
+ }
+
+ @SuppressLint("NewApi")
+ @Override
+ public boolean getBlockNetworkLoads() {
+ final WebViewFeatureInternal feature =
+ WebViewFeatureInternal.SERVICE_WORKER_BLOCK_NETWORK_LOADS;
+ if (feature.isSupportedByFramework()) {
+ return mFrameworksImpl.getBlockNetworkLoads();
+ } else if (feature.isSupportedByWebView()) {
+ return getBoundaryInterface().getBlockNetworkLoads();
+ } else {
+ throw WebViewFeatureInternal.getUnsupportedOperationException();
+ }
+ }
+}
diff --git a/webkit/src/main/java/androidx/webkit/internal/WebkitToCompatConverter.java b/webkit/src/main/java/androidx/webkit/internal/WebkitToCompatConverter.java
index a07cf07..4d264a3 100644
--- a/webkit/src/main/java/androidx/webkit/internal/WebkitToCompatConverter.java
+++ b/webkit/src/main/java/androidx/webkit/internal/WebkitToCompatConverter.java
@@ -16,8 +16,10 @@
package androidx.webkit.internal;
+import android.webkit.ServiceWorkerWebSettings;
import android.webkit.WebSettings;
+import org.chromium.support_lib_boundary.ServiceWorkerWebSettingsBoundaryInterface;
import org.chromium.support_lib_boundary.WebSettingsBoundaryInterface;
import org.chromium.support_lib_boundary.WebkitToCompatConverterBoundaryInterface;
import org.chromium.support_lib_boundary.util.BoundaryInterfaceReflectionUtil;
@@ -42,4 +44,16 @@
return new WebSettingsAdapter(BoundaryInterfaceReflectionUtil.castToSuppLibClass(
WebSettingsBoundaryInterface.class, mImpl.convertSettings(webSettings)));
}
+
+ /**
+ * Return a {@link ServiceWorkerWebSettingsBoundaryInterface} linked to the given
+ * {@link ServiceWorkerWebSettings }such that calls on either of those objects affect the other
+ * object.
+ */
+ public ServiceWorkerWebSettingsBoundaryInterface convertServiceWorkerSettings(
+ ServiceWorkerWebSettings settings) {
+ return BoundaryInterfaceReflectionUtil.castToSuppLibClass(
+ ServiceWorkerWebSettingsBoundaryInterface.class,
+ mImpl.convertServiceWorkerSettings(settings));
+ }
}