Merge "Support7Demos: media router sample" into jb-mr2-ub-dev
@@ -21,6 +21,12 @@
|
|||||||
to come from a domain that you own or have control over. -->
|
to come from a domain that you own or have control over. -->
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="com.example.android.supportv7">
|
package="com.example.android.supportv7">
|
||||||
|
<!-- Permission for INTERNET is required for streaming video content
|
||||||
|
from the web, it's not required otherwise. -->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<!-- Permission for SYSTEM_ALERT_WINDOW is only required for emulating
|
||||||
|
remote display using system alert window. -->
|
||||||
|
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||||
|
|
||||||
<uses-sdk android:minSdkVersion="7" android:targetSdkVersion="17" />
|
<uses-sdk android:minSdkVersion="7" android:targetSdkVersion="17" />
|
||||||
|
|
||||||
|
|||||||
BIN
samples/Support7Demos/res/drawable-hdpi/ic_media_pause.png
Normal file
|
After Width: | Height: | Size: 599 B |
BIN
samples/Support7Demos/res/drawable-hdpi/ic_media_play.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
samples/Support7Demos/res/drawable-hdpi/ic_media_stop.png
Normal file
|
After Width: | Height: | Size: 553 B |
BIN
samples/Support7Demos/res/drawable-hdpi/ic_menu_add.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
samples/Support7Demos/res/drawable-hdpi/ic_menu_delete.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
samples/Support7Demos/res/drawable-mdpi/ic_media_pause.png
Normal file
|
After Width: | Height: | Size: 540 B |
BIN
samples/Support7Demos/res/drawable-mdpi/ic_media_play.png
Normal file
|
After Width: | Height: | Size: 897 B |
BIN
samples/Support7Demos/res/drawable-mdpi/ic_media_stop.png
Normal file
|
After Width: | Height: | Size: 500 B |
BIN
samples/Support7Demos/res/drawable-mdpi/ic_menu_add.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
samples/Support7Demos/res/drawable-mdpi/ic_menu_delete.png
Normal file
|
After Width: | Height: | Size: 967 B |
43
samples/Support7Demos/res/layout/media_item.xml
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Copyright (C) 2012 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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- Layout for list item in Library or Playlist view. Displays ImageButton
|
||||||
|
instead of radio button to the right of the item. -->
|
||||||
|
|
||||||
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
|
<ImageButton android:id="@+id/item_action"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:minWidth="48dp"
|
||||||
|
android:minHeight="48dp"
|
||||||
|
android:layout_alignParentRight="true"
|
||||||
|
android:layout_centerVertical="true"
|
||||||
|
android:background="@null"/>
|
||||||
|
|
||||||
|
<TextView android:id="@+id/item_text"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||||
|
android:layout_centerVertical="true"
|
||||||
|
android:layout_toLeftOf="@id/item_action"
|
||||||
|
android:layout_gravity="left"
|
||||||
|
android:gravity="left"/>
|
||||||
|
</RelativeLayout>
|
||||||
28
samples/Support7Demos/res/layout/overlay_display_window.xml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Copyright (C) 2012 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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="#000000">
|
||||||
|
<TextureView android:id="@+id/overlay_display_window_texture"
|
||||||
|
android:layout_width="0px"
|
||||||
|
android:layout_height="0px" />
|
||||||
|
<TextView android:id="@+id/overlay_display_window_title"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="top|center_horizontal" />
|
||||||
|
</FrameLayout>
|
||||||
@@ -20,45 +20,118 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
<LinearLayout
|
||||||
<!-- Message to show to use. -->
|
|
||||||
<TextView android:id="@+id/text"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="0dp"
|
||||||
android:layout_weight="0"
|
android:layout_weight="1"
|
||||||
android:gravity="center_vertical|center_horizontal"
|
android:orientation="vertical">
|
||||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
<!-- Tabs for media library, playlist and statistics -->
|
||||||
android:text="@string/sample_media_router_text"/>
|
<TabHost android:id="@+id/tabHost"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="fill_parent"
|
||||||
|
android:layout_weight="1">
|
||||||
|
<LinearLayout
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="fill_parent">
|
||||||
|
<TabWidget android:id="@android:id/tabs"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
<!-- Some information about what's going on. -->
|
<FrameLayout android:id="@android:id/tabcontent"
|
||||||
<TextView android:id="@+id/info"
|
android:layout_width="fill_parent"
|
||||||
android:layout_width="match_parent"
|
android:layout_height="wrap_content">
|
||||||
|
<LinearLayout android:id="@+id/tab1"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="0"
|
android:orientation="vertical">
|
||||||
android:gravity="center_vertical|center_horizontal"
|
|
||||||
android:textAppearance="?android:attr/textAppearanceMedium"/>
|
|
||||||
|
|
||||||
<!-- Some media to play. -->
|
|
||||||
<ListView android:id="@+id/media"
|
<ListView android:id="@+id/media"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout android:id="@+id/tab2"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="fill_parent"
|
||||||
|
android:orientation="vertical">
|
||||||
|
<ListView android:id="@+id/playlist"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="1"/>
|
android:layout_weight="1"/>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout android:id="@+id/tab3"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="fill_parent"
|
||||||
|
android:orientation="vertical">
|
||||||
|
<TextView android:id="@+id/info"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceMedium"/>
|
||||||
|
</LinearLayout>
|
||||||
|
</FrameLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
</TabHost>
|
||||||
|
|
||||||
<!-- Control buttons for the currently selected route. -->
|
<!-- Control buttons for the currently selected route. -->
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="0">
|
android:layout_weight="0">
|
||||||
<Button android:id="@+id/play_button"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_weight="0"
|
|
||||||
android:text="@string/play_button_text" />
|
|
||||||
|
|
||||||
<Button android:id="@+id/statistics_button"
|
<SeekBar android:id="@+id/seekbar"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
style="?android:attr/progressBarStyleHorizontal"
|
||||||
|
android:max="100"
|
||||||
|
android:progress="0"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_weight="1"/>
|
||||||
|
|
||||||
|
<ImageButton android:id="@+id/pause_resume_button"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="0"
|
android:layout_weight="0"
|
||||||
android:text="@string/statistics_button_text" />
|
android:layout_gravity="right"
|
||||||
|
android:minWidth="48dp"
|
||||||
|
android:minHeight="48dp"
|
||||||
|
android:background="@null"
|
||||||
|
android:src="@drawable/ic_media_pause" />
|
||||||
|
|
||||||
|
<ImageButton android:id="@+id/stop_button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="0"
|
||||||
|
android:layout_gravity="right"
|
||||||
|
android:minWidth="48dp"
|
||||||
|
android:minHeight="48dp"
|
||||||
|
android:background="@null"
|
||||||
|
android:src="@drawable/ic_media_stop" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Some content for visual interest in the case where no presentation is showing. -->
|
||||||
|
<FrameLayout android:id="@+id/player"
|
||||||
|
android:background="#ff000000"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1">
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center">
|
||||||
|
<SurfaceView android:id="@+id/surface_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"/>
|
||||||
|
</LinearLayout>
|
||||||
|
<TextView
|
||||||
|
android:textColor="#ffaaaaaa"
|
||||||
|
android:text="@string/sample_media_route_activity_local"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="top|center_horizontal" />
|
||||||
|
</FrameLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Copyright (C) 2013 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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- The content that we show on secondary displays.
|
||||||
|
See corresponding Java code PresentationWithMediaRouterActivity.java. -->
|
||||||
|
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="#ff000000">
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center">
|
||||||
|
<SurfaceView android:id="@+id/surface_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"/>
|
||||||
|
</LinearLayout>
|
||||||
|
<TextView
|
||||||
|
android:textColor="#ffaaaaaa"
|
||||||
|
android:text="@string/sample_media_route_activity_presentation"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="top|center_horizontal" />
|
||||||
|
</FrameLayout>
|
||||||
@@ -16,18 +16,16 @@
|
|||||||
|
|
||||||
<resources>
|
<resources>
|
||||||
<string-array name="media_names">
|
<string-array name="media_names">
|
||||||
<item>My favorite video of cats</item>
|
<item>Big Buck Bunny</item>
|
||||||
<item>Cats on parade</item>
|
<item>Elephants Dream</item>
|
||||||
<item>Cats with hats</item>
|
<item>Sintel</item>
|
||||||
<item>Hats on cats</item>
|
<item>Tears of Steel</item>
|
||||||
<item>Cats on mats</item>
|
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
<string-array name="media_uris">
|
<string-array name="media_uris">
|
||||||
<item>http://cats.example.com/favorite-cats.mp4</item>
|
<item>http://archive.org/download/BigBuckBunny_328/BigBuckBunny_512kb.mp4</item>
|
||||||
<item>http://cats.example.com/cats-on-parade.mp4</item>
|
<item>http://archive.org/download/ElephantsDream_277/elephant_dreams_640_512kb.mp4</item>
|
||||||
<item>http://cats.example.com/cats-with-hats.mp4</item>
|
<item>http://archive.org/download/Sintel/sintel-2048-stereo_512kb.mp4</item>
|
||||||
<item>http://cats.example.com/hats-on-cats.mp4</item>
|
<item>http://archive.org/download/Tears-of-Steel/tears_of_steel_720p.mp4</item>
|
||||||
<item>http://cats.example.com/cats-on-mats.mp4</item>
|
|
||||||
</string-array>
|
</string-array>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -26,8 +26,9 @@
|
|||||||
use MediaRouter from the support library. Select a route from the action bar.</string>
|
use MediaRouter from the support library. Select a route from the action bar.</string>
|
||||||
<string name="media_route_menu_title">Play on...</string>
|
<string name="media_route_menu_title">Play on...</string>
|
||||||
|
|
||||||
<string name="play_button_text">Play</string>
|
<string name="library_tab_text">Library</string>
|
||||||
<string name="statistics_button_text">Show Statistics</string>
|
<string name="playlist_tab_text">Playlist</string>
|
||||||
|
<string name="statistics_tab_text">Statistics</string>
|
||||||
|
|
||||||
<string name="sample_media_route_provider_service">Media Route Provider Service Support Library Sample</string>
|
<string name="sample_media_route_provider_service">Media Route Provider Service Support Library Sample</string>
|
||||||
<string name="fixed_volume_route_name">Fixed Volume Remote Playback Route</string>
|
<string name="fixed_volume_route_name">Fixed Volume Remote Playback Route</string>
|
||||||
@@ -88,4 +89,8 @@
|
|||||||
<string name="action_bar_fragment_has_options_menu">Set has options menu to true</string>
|
<string name="action_bar_fragment_has_options_menu">Set has options menu to true</string>
|
||||||
<string name="action_bar_fragment_menu_visibility">Set menu visibility to true</string>
|
<string name="action_bar_fragment_menu_visibility">Set menu visibility to true</string>
|
||||||
|
|
||||||
|
<string name="sample_media_route_provider_remote">Remote Playback (Simulated)</string>
|
||||||
|
<string name="sample_media_route_activity_local">Local Playback</string>
|
||||||
|
<string name="sample_media_route_activity_presentation">Local Playback on Presentation Display</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -0,0 +1,274 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2013 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.example.android.supportv7.media;
|
||||||
|
|
||||||
|
import com.example.android.supportv7.R;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.media.MediaPlayer;
|
||||||
|
import android.view.Surface;
|
||||||
|
import android.view.Gravity;
|
||||||
|
import android.graphics.SurfaceTexture;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MediaPlayerWrapper handles playback of a single media item, and is used for
|
||||||
|
* both local and remote playback.
|
||||||
|
*/
|
||||||
|
public class MediaPlayerWrapper implements
|
||||||
|
MediaPlayer.OnPreparedListener,
|
||||||
|
MediaPlayer.OnCompletionListener,
|
||||||
|
MediaPlayer.OnErrorListener,
|
||||||
|
MediaPlayer.OnSeekCompleteListener,
|
||||||
|
OverlayDisplayWindow.OverlayWindowListener,
|
||||||
|
MediaSessionManager.Callback {
|
||||||
|
private static final String TAG = "MediaPlayerWrapper";
|
||||||
|
private static final boolean DEBUG = false;
|
||||||
|
|
||||||
|
private static final int STATE_IDLE = 0;
|
||||||
|
private static final int STATE_PLAY_PENDING = 1;
|
||||||
|
private static final int STATE_READY = 2;
|
||||||
|
private static final int STATE_PLAYING = 3;
|
||||||
|
private static final int STATE_PAUSED = 4;
|
||||||
|
|
||||||
|
private final Context mContext;
|
||||||
|
private final Handler mHandler = new Handler();
|
||||||
|
private MediaPlayer mMediaPlayer;
|
||||||
|
private int mState = STATE_IDLE;
|
||||||
|
private Callback mCallback;
|
||||||
|
private Surface mSurface;
|
||||||
|
private int mSeekToPos;
|
||||||
|
|
||||||
|
public MediaPlayerWrapper(Context context) {
|
||||||
|
mContext = context;
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void release() {
|
||||||
|
onStop();
|
||||||
|
mMediaPlayer.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCallback(Callback cb) {
|
||||||
|
mCallback = cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
// MediaSessionManager.Callback
|
||||||
|
@Override
|
||||||
|
public void onNewItem(Uri uri) {
|
||||||
|
reset();
|
||||||
|
try {
|
||||||
|
mMediaPlayer.setDataSource(mContext, uri);
|
||||||
|
mMediaPlayer.prepareAsync();
|
||||||
|
} catch (IllegalStateException e) {
|
||||||
|
Log.e(TAG, "MediaPlayer throws IllegalStateException, uri=" + uri);
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.e(TAG, "MediaPlayer throws IOException, uri=" + uri);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
Log.e(TAG, "MediaPlayer throws IllegalArgumentException, uri=" + uri);
|
||||||
|
} catch (SecurityException e) {
|
||||||
|
Log.e(TAG, "MediaPlayer throws SecurityException, uri=" + uri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStart() {
|
||||||
|
if (mState == STATE_READY || mState == STATE_PAUSED) {
|
||||||
|
mMediaPlayer.start();
|
||||||
|
mState = STATE_PLAYING;
|
||||||
|
} else if (mState == STATE_IDLE){
|
||||||
|
mState = STATE_PLAY_PENDING;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStop() {
|
||||||
|
if (mState == STATE_PLAYING || mState == STATE_PAUSED) {
|
||||||
|
mMediaPlayer.stop();
|
||||||
|
mState = STATE_IDLE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPause() {
|
||||||
|
if (mState == STATE_PLAYING) {
|
||||||
|
mMediaPlayer.pause();
|
||||||
|
mState = STATE_PAUSED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSeek(long pos) {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "onSeek: pos=" + pos);
|
||||||
|
}
|
||||||
|
if (mState == STATE_PLAYING || mState == STATE_PAUSED) {
|
||||||
|
mMediaPlayer.seekTo((int)pos);
|
||||||
|
mSeekToPos = (int)pos;
|
||||||
|
} else if (mState == STATE_IDLE || mState == STATE_PLAY_PENDING) {
|
||||||
|
// Seek before onPrepared() arrives,
|
||||||
|
// need to performed delayed seek in onPrepared()
|
||||||
|
mSeekToPos = (int)pos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onGetStatus(MediaQueueItem item) {
|
||||||
|
if (mState == STATE_PLAYING || mState == STATE_PAUSED) {
|
||||||
|
// use mSeekToPos if we're currently seeking (mSeekToPos is reset
|
||||||
|
// when seeking is completed)
|
||||||
|
item.setContentDuration(mMediaPlayer.getDuration());
|
||||||
|
item.setContentPosition(mSeekToPos > 0 ?
|
||||||
|
mSeekToPos : mMediaPlayer.getCurrentPosition());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//OverlayDisplayWindow listeners
|
||||||
|
@Override
|
||||||
|
public void onWindowCreated(Surface surface) {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "onWindowCreated");
|
||||||
|
}
|
||||||
|
mSurface = surface;
|
||||||
|
mMediaPlayer.setSurface(surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onWindowDestroyed() {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "onWindowDestroyed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//MediaPlayer Listeners
|
||||||
|
@Override
|
||||||
|
public void onPrepared(MediaPlayer mp) {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG,"onPrepared");
|
||||||
|
}
|
||||||
|
mHandler.post(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (mState == STATE_IDLE) {
|
||||||
|
mState = STATE_READY;
|
||||||
|
updateVideoRect();
|
||||||
|
} else if (mState == STATE_PLAY_PENDING) {
|
||||||
|
mState = STATE_PLAYING;
|
||||||
|
updateVideoRect();
|
||||||
|
if (mSeekToPos > 0) {
|
||||||
|
Log.d(TAG, "Seeking to initial pos " + mSeekToPos);
|
||||||
|
mMediaPlayer.seekTo((int)mSeekToPos);
|
||||||
|
}
|
||||||
|
mMediaPlayer.start();
|
||||||
|
}
|
||||||
|
if (mCallback != null) {
|
||||||
|
mCallback.onStatusChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCompletion(MediaPlayer mp) {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG,"onCompletion");
|
||||||
|
}
|
||||||
|
mHandler.post(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (mCallback != null) {
|
||||||
|
mCallback.onCompletion();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onError(MediaPlayer mp, int what, int extra) {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG,"onError");
|
||||||
|
}
|
||||||
|
mHandler.post(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (mCallback != null) {
|
||||||
|
mCallback.onError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// return true so that onCompletion is not called
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSeekComplete(MediaPlayer mp) {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "onSeekComplete");
|
||||||
|
}
|
||||||
|
mHandler.post(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
mSeekToPos = 0;
|
||||||
|
if (mCallback != null) {
|
||||||
|
mCallback.onStatusChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reset() {
|
||||||
|
if (mMediaPlayer != null) {
|
||||||
|
mMediaPlayer.stop();
|
||||||
|
mMediaPlayer.release();
|
||||||
|
mMediaPlayer = null;
|
||||||
|
}
|
||||||
|
mMediaPlayer = new MediaPlayer();
|
||||||
|
mMediaPlayer.setOnPreparedListener(this);
|
||||||
|
mMediaPlayer.setOnCompletionListener(this);
|
||||||
|
mMediaPlayer.setOnErrorListener(this);
|
||||||
|
mMediaPlayer.setOnSeekCompleteListener(this);
|
||||||
|
if (mSurface != null) {
|
||||||
|
mMediaPlayer.setSurface(mSurface);
|
||||||
|
}
|
||||||
|
mState = STATE_IDLE;
|
||||||
|
mSeekToPos = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateVideoRect() {
|
||||||
|
if (mState != STATE_IDLE && mState != STATE_PLAY_PENDING) {
|
||||||
|
int videoWidth = mMediaPlayer.getVideoWidth();
|
||||||
|
int videoHeight = mMediaPlayer.getVideoHeight();
|
||||||
|
if (videoWidth > 0 && videoHeight > 0) {
|
||||||
|
if (mCallback != null) {
|
||||||
|
mCallback.onSizeChanged(videoWidth, videoHeight);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "video rect is 0x0!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static abstract class Callback {
|
||||||
|
public void onError() {}
|
||||||
|
public void onCompletion() {}
|
||||||
|
public void onStatusChanged() {}
|
||||||
|
public void onSizeChanged(int width, int height) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2013 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.example.android.supportv7.media;
|
||||||
|
|
||||||
|
import android.support.v7.media.MediaItemStatus;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MediaQueueItem helps keep track of the current status of an media item.
|
||||||
|
*/
|
||||||
|
final class MediaQueueItem {
|
||||||
|
// immutables
|
||||||
|
private final String mSessionId;
|
||||||
|
private final String mItemId;
|
||||||
|
private final Uri mUri;
|
||||||
|
private final PendingIntent mUpdateReceiver;
|
||||||
|
// changeable states
|
||||||
|
private int mPlaybackState = MediaItemStatus.PLAYBACK_STATE_PENDING;
|
||||||
|
private long mContentPosition;
|
||||||
|
private long mContentDuration;
|
||||||
|
|
||||||
|
public MediaQueueItem(String qid, String iid, Uri uri, PendingIntent pi) {
|
||||||
|
mSessionId = qid;
|
||||||
|
mItemId = iid;
|
||||||
|
mUri = uri;
|
||||||
|
mUpdateReceiver = pi;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setState(int state) {
|
||||||
|
mPlaybackState = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setContentPosition(long pos) {
|
||||||
|
mContentPosition = pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setContentDuration(long duration) {
|
||||||
|
mContentDuration = duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSessionId() {
|
||||||
|
return mSessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getItemId() {
|
||||||
|
return mItemId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Uri getUri() {
|
||||||
|
return mUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PendingIntent getUpdateReceiver() {
|
||||||
|
return mUpdateReceiver;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getState() {
|
||||||
|
return mPlaybackState;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getContentPosition() {
|
||||||
|
return mContentPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getContentDuration() {
|
||||||
|
return mContentDuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MediaItemStatus getStatus() {
|
||||||
|
return new MediaItemStatus.Builder(mPlaybackState)
|
||||||
|
.setContentPosition(mContentPosition)
|
||||||
|
.setContentDuration(mContentDuration)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
String state[] = {
|
||||||
|
"PENDING",
|
||||||
|
"PLAYING",
|
||||||
|
"PAUSED",
|
||||||
|
"BUFFERING",
|
||||||
|
"FINISHED",
|
||||||
|
"CANCELED",
|
||||||
|
"INVALIDATED",
|
||||||
|
"ERROR"
|
||||||
|
};
|
||||||
|
return "[" + mSessionId + "|" + mItemId + "|"
|
||||||
|
+ state[mPlaybackState] + "] " + mUri.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,289 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2013 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.example.android.supportv7.media;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
import android.support.v7.media.MediaItemStatus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MediaSessionManager manages a media session as a queue. It supports common
|
||||||
|
* queuing behaviors such as enqueue/remove of media items, pause/resume/stop,
|
||||||
|
* etc.
|
||||||
|
*
|
||||||
|
* Actual playback of a single media item is abstracted into a set of
|
||||||
|
* callbacks MediaSessionManager.Callback, and is handled outside this class.
|
||||||
|
*/
|
||||||
|
public class MediaSessionManager {
|
||||||
|
private static final String TAG = "MediaSessionManager";
|
||||||
|
|
||||||
|
private String mSessionId;
|
||||||
|
private String mItemId;
|
||||||
|
private String mCurItemId;
|
||||||
|
private boolean mIsPlaying = true;
|
||||||
|
private Callback mCallback;
|
||||||
|
private List<MediaQueueItem> mQueue = new ArrayList<MediaQueueItem>();
|
||||||
|
|
||||||
|
public MediaSessionManager() {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Queue item (this maps to the ENQUEUE in the API which queues the item)
|
||||||
|
public MediaQueueItem enqueue(String sid, Uri uri, PendingIntent receiver) {
|
||||||
|
// fail if queue id is invalid
|
||||||
|
if (sid != null && !sid.equals(mSessionId)) {
|
||||||
|
Log.d(TAG, "invalid session id, mSessionId="+mSessionId+", sid="+sid);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if queue id is unspecified, invalidate current queue
|
||||||
|
if (sid == null) {
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
mQueue.add(new MediaQueueItem(mSessionId, mItemId, uri, receiver));
|
||||||
|
|
||||||
|
if (updatePlaybackState()) {
|
||||||
|
MediaQueueItem item = findItem(mItemId);
|
||||||
|
mItemId = inc(mItemId);
|
||||||
|
if (item == null) {
|
||||||
|
Log.d(TAG, "item not found after it's added");
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeItem(mItemId, MediaItemStatus.PLAYBACK_STATE_ERROR);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MediaQueueItem remove(String sid, String iid) {
|
||||||
|
if (sid == null || !sid.equals(mSessionId)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return removeItem(iid, MediaItemStatus.PLAYBACK_STATE_CANCELED);
|
||||||
|
}
|
||||||
|
|
||||||
|
// handles ERROR / COMPLETION
|
||||||
|
public MediaQueueItem finish(boolean error) {
|
||||||
|
return removeItem(mCurItemId, error ? MediaItemStatus.PLAYBACK_STATE_ERROR :
|
||||||
|
MediaItemStatus.PLAYBACK_STATE_FINISHED);
|
||||||
|
}
|
||||||
|
|
||||||
|
public MediaQueueItem seek(String sid, String iid, long pos) {
|
||||||
|
if (sid == null || !sid.equals(mSessionId)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < mQueue.size(); i++) {
|
||||||
|
MediaQueueItem item = mQueue.get(i);
|
||||||
|
if (iid.equals(item.getItemId())) {
|
||||||
|
if (pos != item.getContentPosition()) {
|
||||||
|
item.setContentPosition(pos);
|
||||||
|
if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PLAYING
|
||||||
|
|| item.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED) {
|
||||||
|
if (mCallback != null) {
|
||||||
|
mCallback.onSeek(pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MediaQueueItem getCurrentItem() {
|
||||||
|
return getStatus(mSessionId, mCurItemId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public MediaQueueItem getStatus(String sid, String iid) {
|
||||||
|
if (sid == null || !sid.equals(mSessionId)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < mQueue.size(); i++) {
|
||||||
|
MediaQueueItem item = mQueue.get(i);
|
||||||
|
if (iid.equals(item.getItemId())) {
|
||||||
|
if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PLAYING
|
||||||
|
|| item.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED) {
|
||||||
|
if (mCallback != null) {
|
||||||
|
mCallback.onGetStatus(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean pause(String sid) {
|
||||||
|
if (sid == null || !sid.equals(mSessionId)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
mIsPlaying = false;
|
||||||
|
return updatePlaybackState();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean resume(String sid) {
|
||||||
|
if (sid == null || !sid.equals(mSessionId)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
mIsPlaying = true;
|
||||||
|
return updatePlaybackState();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean stop(String sid) {
|
||||||
|
if (sid == null || !sid.equals(mSessionId)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
clear();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCallback(Callback cb) {
|
||||||
|
mCallback = cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
String result = "Media Queue: ";
|
||||||
|
if (!mQueue.isEmpty()) {
|
||||||
|
for (MediaQueueItem item : mQueue) {
|
||||||
|
result += "\n" + item.toString();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result += "<empty>";
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String inc(String id) {
|
||||||
|
return (id == null) ? "0" : Integer.toString(Integer.parseInt(id)+1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// play the item at queue head
|
||||||
|
private void play() {
|
||||||
|
MediaQueueItem item = mQueue.get(0);
|
||||||
|
if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PENDING
|
||||||
|
|| item.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED) {
|
||||||
|
mCurItemId = item.getItemId();
|
||||||
|
if (mCallback != null) {
|
||||||
|
if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PENDING) {
|
||||||
|
mCallback.onNewItem(item.getUri());
|
||||||
|
}
|
||||||
|
mCallback.onStart();
|
||||||
|
}
|
||||||
|
item.setState(MediaItemStatus.PLAYBACK_STATE_PLAYING);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// stop the currently playing item
|
||||||
|
private void stop() {
|
||||||
|
MediaQueueItem item = mQueue.get(0);
|
||||||
|
if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PLAYING
|
||||||
|
|| item.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED) {
|
||||||
|
if (mCallback != null) {
|
||||||
|
mCallback.onStop();
|
||||||
|
}
|
||||||
|
item.setState(MediaItemStatus.PLAYBACK_STATE_FINISHED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pause the currently playing item
|
||||||
|
private void pause() {
|
||||||
|
MediaQueueItem item = mQueue.get(0);
|
||||||
|
if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PENDING
|
||||||
|
|| item.getState() == MediaItemStatus.PLAYBACK_STATE_PLAYING) {
|
||||||
|
if (mCallback != null) {
|
||||||
|
if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PENDING) {
|
||||||
|
mCallback.onNewItem(item.getUri());
|
||||||
|
} else if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PLAYING) {
|
||||||
|
mCallback.onPause();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
item.setState(MediaItemStatus.PLAYBACK_STATE_PAUSED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clear() {
|
||||||
|
if (mQueue.size() > 0) {
|
||||||
|
stop();
|
||||||
|
mQueue.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void invalidate() {
|
||||||
|
clear();
|
||||||
|
mSessionId = inc(mSessionId);
|
||||||
|
mItemId = "0";
|
||||||
|
mIsPlaying = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean updatePlaybackState() {
|
||||||
|
if (mQueue.isEmpty()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mIsPlaying) {
|
||||||
|
play();
|
||||||
|
} else {
|
||||||
|
pause();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private MediaQueueItem findItem(String iid) {
|
||||||
|
for (MediaQueueItem item : mQueue) {
|
||||||
|
if (iid.equals(item.getItemId())) {
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private MediaQueueItem removeItem(String iid, int state) {
|
||||||
|
List<MediaQueueItem> queue =
|
||||||
|
new ArrayList<MediaQueueItem>(mQueue.size());
|
||||||
|
MediaQueueItem found = null;
|
||||||
|
for (MediaQueueItem item : mQueue) {
|
||||||
|
if (iid.equals(item.getItemId())) {
|
||||||
|
if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PLAYING
|
||||||
|
|| item.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED) {
|
||||||
|
stop();
|
||||||
|
}
|
||||||
|
item.setState(state);
|
||||||
|
found = item;
|
||||||
|
} else {
|
||||||
|
queue.add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (found != null) {
|
||||||
|
mQueue = queue;
|
||||||
|
updatePlaybackState();
|
||||||
|
}
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface Callback {
|
||||||
|
public void onStart();
|
||||||
|
public void onPause();
|
||||||
|
public void onStop();
|
||||||
|
public void onSeek(long pos);
|
||||||
|
public void onGetStatus(MediaQueueItem item);
|
||||||
|
public void onNewItem(Uri uri);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,362 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2012 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.example.android.supportv7.media;
|
||||||
|
import com.example.android.supportv7.R;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.SurfaceTexture;
|
||||||
|
import android.hardware.display.DisplayManager;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.Display;
|
||||||
|
import android.util.DisplayMetrics;
|
||||||
|
import android.view.GestureDetector;
|
||||||
|
import android.view.Gravity;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.ScaleGestureDetector;
|
||||||
|
import android.view.TextureView;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.Surface;
|
||||||
|
import android.view.WindowManager;
|
||||||
|
import android.view.TextureView.SurfaceTextureListener;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages an overlay display window, used for simulating remote playback.
|
||||||
|
*/
|
||||||
|
public class OverlayDisplayWindow {
|
||||||
|
private static final String TAG = "OverlayDisplayWindow";
|
||||||
|
private static final boolean DEBUG = false;
|
||||||
|
|
||||||
|
private final float INITIAL_SCALE = 0.5f;
|
||||||
|
private final float MIN_SCALE = 0.3f;
|
||||||
|
private final float MAX_SCALE = 1.0f;
|
||||||
|
private final float WINDOW_ALPHA = 0.8f;
|
||||||
|
|
||||||
|
// When true, disables support for moving and resizing the overlay.
|
||||||
|
// The window is made non-touchable, which makes it possible to
|
||||||
|
// directly interact with the content underneath.
|
||||||
|
private final boolean DISABLE_MOVE_AND_RESIZE = false;
|
||||||
|
|
||||||
|
private final Context mContext;
|
||||||
|
private final int mWidth;
|
||||||
|
private final int mHeight;
|
||||||
|
private final int mGravity;
|
||||||
|
private OverlayWindowListener mListener;
|
||||||
|
private final String mTitle;
|
||||||
|
|
||||||
|
private final DisplayManager mDisplayManager;
|
||||||
|
private final WindowManager mWindowManager;
|
||||||
|
|
||||||
|
|
||||||
|
private final Display mDefaultDisplay;
|
||||||
|
private final DisplayMetrics mDefaultDisplayMetrics = new DisplayMetrics();
|
||||||
|
|
||||||
|
private View mWindowContent;
|
||||||
|
private WindowManager.LayoutParams mWindowParams;
|
||||||
|
private TextureView mTextureView;
|
||||||
|
private TextView mTitleTextView;
|
||||||
|
|
||||||
|
private GestureDetector mGestureDetector;
|
||||||
|
private ScaleGestureDetector mScaleGestureDetector;
|
||||||
|
|
||||||
|
private boolean mWindowVisible;
|
||||||
|
private int mWindowX;
|
||||||
|
private int mWindowY;
|
||||||
|
private float mWindowScale;
|
||||||
|
|
||||||
|
private float mLiveTranslationX;
|
||||||
|
private float mLiveTranslationY;
|
||||||
|
private float mLiveScale = 1.0f;
|
||||||
|
|
||||||
|
public OverlayDisplayWindow(Context context, String name,
|
||||||
|
int width, int height, int gravity) {
|
||||||
|
mContext = context;
|
||||||
|
mWidth = width;
|
||||||
|
mHeight = height;
|
||||||
|
mGravity = gravity;
|
||||||
|
mTitle = name;
|
||||||
|
|
||||||
|
mDisplayManager = (DisplayManager)context.getSystemService(
|
||||||
|
Context.DISPLAY_SERVICE);
|
||||||
|
mWindowManager = (WindowManager)context.getSystemService(
|
||||||
|
Context.WINDOW_SERVICE);
|
||||||
|
|
||||||
|
mDefaultDisplay = mWindowManager.getDefaultDisplay();
|
||||||
|
updateDefaultDisplayInfo();
|
||||||
|
|
||||||
|
createWindow();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOverlayWindowListener(OverlayWindowListener listener) {
|
||||||
|
mListener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Context getContext() {
|
||||||
|
return mContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void show() {
|
||||||
|
if (!mWindowVisible) {
|
||||||
|
mDisplayManager.registerDisplayListener(mDisplayListener, null);
|
||||||
|
if (!updateDefaultDisplayInfo()) {
|
||||||
|
mDisplayManager.unregisterDisplayListener(mDisplayListener);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearLiveState();
|
||||||
|
updateWindowParams();
|
||||||
|
mWindowManager.addView(mWindowContent, mWindowParams);
|
||||||
|
mWindowVisible = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void dismiss() {
|
||||||
|
if (mWindowVisible) {
|
||||||
|
mDisplayManager.unregisterDisplayListener(mDisplayListener);
|
||||||
|
mWindowManager.removeView(mWindowContent);
|
||||||
|
mWindowVisible = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void relayout() {
|
||||||
|
if (mWindowVisible) {
|
||||||
|
updateWindowParams();
|
||||||
|
mWindowManager.updateViewLayout(mWindowContent, mWindowParams);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateAspectRatio(int width, int height) {
|
||||||
|
if (mWidth * height < mHeight * width) {
|
||||||
|
mTextureView.getLayoutParams().width = mWidth;
|
||||||
|
mTextureView.getLayoutParams().height = mWidth * height / width;
|
||||||
|
} else {
|
||||||
|
mTextureView.getLayoutParams().width = mHeight * width / height;
|
||||||
|
mTextureView.getLayoutParams().height = mHeight;
|
||||||
|
}
|
||||||
|
relayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean updateDefaultDisplayInfo() {
|
||||||
|
mDefaultDisplay.getMetrics(mDefaultDisplayMetrics);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createWindow() {
|
||||||
|
LayoutInflater inflater = LayoutInflater.from(mContext);
|
||||||
|
|
||||||
|
mWindowContent = inflater.inflate(
|
||||||
|
R.layout.overlay_display_window, null);
|
||||||
|
mWindowContent.setOnTouchListener(mOnTouchListener);
|
||||||
|
|
||||||
|
mTextureView = (TextureView)mWindowContent.findViewById(
|
||||||
|
R.id.overlay_display_window_texture);
|
||||||
|
mTextureView.setPivotX(0);
|
||||||
|
mTextureView.setPivotY(0);
|
||||||
|
mTextureView.getLayoutParams().width = mWidth;
|
||||||
|
mTextureView.getLayoutParams().height = mHeight;
|
||||||
|
mTextureView.setOpaque(false);
|
||||||
|
mTextureView.setSurfaceTextureListener(mSurfaceTextureListener);
|
||||||
|
|
||||||
|
mTitleTextView = (TextView)mWindowContent.findViewById(
|
||||||
|
R.id.overlay_display_window_title);
|
||||||
|
mTitleTextView.setText(mTitle);
|
||||||
|
|
||||||
|
mWindowParams = new WindowManager.LayoutParams(
|
||||||
|
WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
|
||||||
|
mWindowParams.flags |= WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
|
||||||
|
| WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
|
||||||
|
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
|
||||||
|
| WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
|
||||||
|
| WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
|
||||||
|
if (DISABLE_MOVE_AND_RESIZE) {
|
||||||
|
mWindowParams.flags |= WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
|
||||||
|
}
|
||||||
|
mWindowParams.alpha = WINDOW_ALPHA;
|
||||||
|
mWindowParams.gravity = Gravity.TOP | Gravity.LEFT;
|
||||||
|
mWindowParams.setTitle(mTitle);
|
||||||
|
|
||||||
|
mGestureDetector = new GestureDetector(mContext, mOnGestureListener);
|
||||||
|
mScaleGestureDetector = new ScaleGestureDetector(mContext, mOnScaleGestureListener);
|
||||||
|
|
||||||
|
// Set the initial position and scale.
|
||||||
|
// The position and scale will be clamped when the display is first shown.
|
||||||
|
mWindowX = (mGravity & Gravity.LEFT) == Gravity.LEFT ?
|
||||||
|
0 : mDefaultDisplayMetrics.widthPixels;
|
||||||
|
mWindowY = (mGravity & Gravity.TOP) == Gravity.TOP ?
|
||||||
|
0 : mDefaultDisplayMetrics.heightPixels;
|
||||||
|
Log.d(TAG, mDefaultDisplayMetrics.toString());
|
||||||
|
mWindowScale = INITIAL_SCALE;
|
||||||
|
|
||||||
|
// calculate and save initial settings
|
||||||
|
updateWindowParams();
|
||||||
|
saveWindowParams();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateWindowParams() {
|
||||||
|
float scale = mWindowScale * mLiveScale;
|
||||||
|
scale = Math.min(scale, (float)mDefaultDisplayMetrics.widthPixels / mWidth);
|
||||||
|
scale = Math.min(scale, (float)mDefaultDisplayMetrics.heightPixels / mHeight);
|
||||||
|
scale = Math.max(MIN_SCALE, Math.min(MAX_SCALE, scale));
|
||||||
|
|
||||||
|
float offsetScale = (scale / mWindowScale - 1.0f) * 0.5f;
|
||||||
|
int width = (int)(mWidth * scale);
|
||||||
|
int height = (int)(mHeight * scale);
|
||||||
|
int x = (int)(mWindowX + mLiveTranslationX - width * offsetScale);
|
||||||
|
int y = (int)(mWindowY + mLiveTranslationY - height * offsetScale);
|
||||||
|
x = Math.max(0, Math.min(x, mDefaultDisplayMetrics.widthPixels - width));
|
||||||
|
y = Math.max(0, Math.min(y, mDefaultDisplayMetrics.heightPixels - height));
|
||||||
|
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "updateWindowParams: scale=" + scale
|
||||||
|
+ ", offsetScale=" + offsetScale
|
||||||
|
+ ", x=" + x + ", y=" + y
|
||||||
|
+ ", width=" + width + ", height=" + height);
|
||||||
|
}
|
||||||
|
|
||||||
|
mTextureView.setScaleX(scale);
|
||||||
|
mTextureView.setScaleY(scale);
|
||||||
|
|
||||||
|
mTextureView.setTranslationX(
|
||||||
|
(mWidth - mTextureView.getLayoutParams().width) * scale / 2);
|
||||||
|
mTextureView.setTranslationY(
|
||||||
|
(mHeight - mTextureView.getLayoutParams().height) * scale / 2);
|
||||||
|
|
||||||
|
mWindowParams.x = x;
|
||||||
|
mWindowParams.y = y;
|
||||||
|
mWindowParams.width = width;
|
||||||
|
mWindowParams.height = height;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveWindowParams() {
|
||||||
|
mWindowX = mWindowParams.x;
|
||||||
|
mWindowY = mWindowParams.y;
|
||||||
|
mWindowScale = mTextureView.getScaleX();
|
||||||
|
clearLiveState();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearLiveState() {
|
||||||
|
mLiveTranslationX = 0f;
|
||||||
|
mLiveTranslationY = 0f;
|
||||||
|
mLiveScale = 1.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final DisplayManager.DisplayListener mDisplayListener =
|
||||||
|
new DisplayManager.DisplayListener() {
|
||||||
|
@Override
|
||||||
|
public void onDisplayAdded(int displayId) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDisplayChanged(int displayId) {
|
||||||
|
if (displayId == mDefaultDisplay.getDisplayId()) {
|
||||||
|
if (updateDefaultDisplayInfo()) {
|
||||||
|
relayout();
|
||||||
|
} else {
|
||||||
|
dismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDisplayRemoved(int displayId) {
|
||||||
|
if (displayId == mDefaultDisplay.getDisplayId()) {
|
||||||
|
dismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private final SurfaceTextureListener mSurfaceTextureListener =
|
||||||
|
new SurfaceTextureListener() {
|
||||||
|
@Override
|
||||||
|
public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture,
|
||||||
|
int width, int height) {
|
||||||
|
if (mListener != null) {
|
||||||
|
mListener.onWindowCreated(new Surface(surfaceTexture));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
|
||||||
|
if (mListener != null) {
|
||||||
|
mListener.onWindowDestroyed();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture,
|
||||||
|
int width, int height) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private final View.OnTouchListener mOnTouchListener = new View.OnTouchListener() {
|
||||||
|
@Override
|
||||||
|
public boolean onTouch(View view, MotionEvent event) {
|
||||||
|
// Work in screen coordinates.
|
||||||
|
final float oldX = event.getX();
|
||||||
|
final float oldY = event.getY();
|
||||||
|
event.setLocation(event.getRawX(), event.getRawY());
|
||||||
|
|
||||||
|
mGestureDetector.onTouchEvent(event);
|
||||||
|
mScaleGestureDetector.onTouchEvent(event);
|
||||||
|
|
||||||
|
switch (event.getActionMasked()) {
|
||||||
|
case MotionEvent.ACTION_UP:
|
||||||
|
case MotionEvent.ACTION_CANCEL:
|
||||||
|
saveWindowParams();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Revert to window coordinates.
|
||||||
|
event.setLocation(oldX, oldY);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private final GestureDetector.OnGestureListener mOnGestureListener =
|
||||||
|
new GestureDetector.SimpleOnGestureListener() {
|
||||||
|
@Override
|
||||||
|
public boolean onScroll(MotionEvent e1, MotionEvent e2,
|
||||||
|
float distanceX, float distanceY) {
|
||||||
|
mLiveTranslationX -= distanceX;
|
||||||
|
mLiveTranslationY -= distanceY;
|
||||||
|
relayout();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private final ScaleGestureDetector.OnScaleGestureListener mOnScaleGestureListener =
|
||||||
|
new ScaleGestureDetector.SimpleOnScaleGestureListener() {
|
||||||
|
@Override
|
||||||
|
public boolean onScale(ScaleGestureDetector detector) {
|
||||||
|
mLiveScale *= detector.getScaleFactor();
|
||||||
|
relayout();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Watches for significant changes in the overlay display window lifecycle.
|
||||||
|
public interface OverlayWindowListener {
|
||||||
|
public void onWindowCreated(Surface surface);
|
||||||
|
public void onWindowDestroyed();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,6 +26,7 @@ import android.content.res.Resources;
|
|||||||
import android.media.MediaRouter;
|
import android.media.MediaRouter;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.app.PendingIntent;
|
||||||
import android.support.v7.media.MediaControlIntent;
|
import android.support.v7.media.MediaControlIntent;
|
||||||
import android.support.v7.media.MediaItemStatus;
|
import android.support.v7.media.MediaItemStatus;
|
||||||
import android.support.v7.media.MediaRouteProvider;
|
import android.support.v7.media.MediaRouteProvider;
|
||||||
@@ -34,9 +35,8 @@ import android.support.v7.media.MediaRouteProviderDescriptor;
|
|||||||
import android.support.v7.media.MediaRouteDescriptor;
|
import android.support.v7.media.MediaRouteDescriptor;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
import android.view.Gravity;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Demonstrates how to create a custom media route provider.
|
* Demonstrates how to create a custom media route provider.
|
||||||
@@ -77,6 +77,14 @@ final class SampleMediaRouteProvider extends MediaRouteProvider {
|
|||||||
public static final String DATA_PLAYBACK_COUNT =
|
public static final String DATA_PLAYBACK_COUNT =
|
||||||
"com.example.android.supportv7.media.EXTRA_PLAYBACK_COUNT";
|
"com.example.android.supportv7.media.EXTRA_PLAYBACK_COUNT";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Set ENABLE_QUEUEING to true to test queuing on MRP. This will make
|
||||||
|
* MRP expose the following two experimental hidden APIs:
|
||||||
|
* ACTION_ENQUEUE
|
||||||
|
* ACTION_REMOVE
|
||||||
|
*/
|
||||||
|
public static final boolean ENABLE_QUEUEING = false;
|
||||||
|
|
||||||
private static final ArrayList<IntentFilter> CONTROL_FILTERS;
|
private static final ArrayList<IntentFilter> CONTROL_FILTERS;
|
||||||
static {
|
static {
|
||||||
IntentFilter f1 = new IntentFilter();
|
IntentFilter f1 = new IntentFilter();
|
||||||
@@ -88,11 +96,39 @@ final class SampleMediaRouteProvider extends MediaRouteProvider {
|
|||||||
f2.addAction(MediaControlIntent.ACTION_PLAY);
|
f2.addAction(MediaControlIntent.ACTION_PLAY);
|
||||||
f2.addDataScheme("http");
|
f2.addDataScheme("http");
|
||||||
f2.addDataScheme("https");
|
f2.addDataScheme("https");
|
||||||
|
f2.addDataScheme("rtsp");
|
||||||
|
f2.addDataScheme("file");
|
||||||
addDataTypeUnchecked(f2, "video/*");
|
addDataTypeUnchecked(f2, "video/*");
|
||||||
|
|
||||||
|
IntentFilter f3 = new IntentFilter();
|
||||||
|
f3.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
|
||||||
|
f3.addAction(MediaControlIntent.ACTION_SEEK);
|
||||||
|
f3.addAction(MediaControlIntent.ACTION_GET_STATUS);
|
||||||
|
f3.addAction(MediaControlIntent.ACTION_PAUSE);
|
||||||
|
f3.addAction(MediaControlIntent.ACTION_RESUME);
|
||||||
|
f3.addAction(MediaControlIntent.ACTION_STOP);
|
||||||
|
|
||||||
|
IntentFilter f4 = new IntentFilter();
|
||||||
|
f4.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
|
||||||
|
f4.addAction(MediaControlIntent.ACTION_ENQUEUE);
|
||||||
|
f4.addDataScheme("http");
|
||||||
|
f4.addDataScheme("https");
|
||||||
|
f4.addDataScheme("rtsp");
|
||||||
|
f4.addDataScheme("file");
|
||||||
|
addDataTypeUnchecked(f4, "video/*");
|
||||||
|
|
||||||
|
IntentFilter f5 = new IntentFilter();
|
||||||
|
f5.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
|
||||||
|
f5.addAction(MediaControlIntent.ACTION_REMOVE);
|
||||||
|
|
||||||
CONTROL_FILTERS = new ArrayList<IntentFilter>();
|
CONTROL_FILTERS = new ArrayList<IntentFilter>();
|
||||||
CONTROL_FILTERS.add(f1);
|
CONTROL_FILTERS.add(f1);
|
||||||
CONTROL_FILTERS.add(f2);
|
CONTROL_FILTERS.add(f2);
|
||||||
|
CONTROL_FILTERS.add(f3);
|
||||||
|
if (ENABLE_QUEUEING) {
|
||||||
|
CONTROL_FILTERS.add(f4);
|
||||||
|
CONTROL_FILTERS.add(f5);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void addDataTypeUnchecked(IntentFilter filter, String type) {
|
private static void addDataTypeUnchecked(IntentFilter filter, String type) {
|
||||||
@@ -104,7 +140,7 @@ final class SampleMediaRouteProvider extends MediaRouteProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private int mVolume = 5;
|
private int mVolume = 5;
|
||||||
private int mPlaybackCount;
|
private int mEnqueueCount;
|
||||||
|
|
||||||
public SampleMediaRouteProvider(Context context) {
|
public SampleMediaRouteProvider(Context context) {
|
||||||
super(context);
|
super(context);
|
||||||
@@ -149,31 +185,47 @@ final class SampleMediaRouteProvider extends MediaRouteProvider {
|
|||||||
setDescriptor(providerDescriptor);
|
setDescriptor(providerDescriptor);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String generateStreamId() {
|
private void showToast(String msg) {
|
||||||
return UUID.randomUUID().toString();
|
Toast toast = Toast.makeText(getContext(),
|
||||||
|
"[provider] " + msg, Toast.LENGTH_LONG);
|
||||||
|
toast.setGravity(Gravity.TOP, 0, 100);
|
||||||
|
toast.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class SampleRouteController extends MediaRouteProvider.RouteController {
|
private final class SampleRouteController extends MediaRouteProvider.RouteController {
|
||||||
private final String mRouteId;
|
private final String mRouteId;
|
||||||
|
// Create an overlay display window (used for simulating the remote playback only)
|
||||||
|
private final OverlayDisplayWindow mOverlay = new OverlayDisplayWindow(getContext(),
|
||||||
|
getContext().getResources().getString(R.string.sample_media_route_provider_remote),
|
||||||
|
1024, 768, Gravity.CENTER);
|
||||||
|
private final MediaPlayerWrapper mMediaPlayer = new MediaPlayerWrapper(getContext());
|
||||||
|
private final MediaSessionManager mSessionManager = new MediaSessionManager();
|
||||||
|
|
||||||
public SampleRouteController(String routeId) {
|
public SampleRouteController(String routeId) {
|
||||||
mRouteId = routeId;
|
mRouteId = routeId;
|
||||||
|
mSessionManager.setCallback(mMediaPlayer);
|
||||||
|
mOverlay.setOverlayWindowListener(mMediaPlayer);
|
||||||
|
mMediaPlayer.setCallback(new MediaPlayerCallback());
|
||||||
Log.d(TAG, mRouteId + ": Controller created");
|
Log.d(TAG, mRouteId + ": Controller created");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onRelease() {
|
public void onRelease() {
|
||||||
Log.d(TAG, mRouteId + ": Controller released");
|
Log.d(TAG, mRouteId + ": Controller released");
|
||||||
|
mMediaPlayer.release();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSelect() {
|
public void onSelect() {
|
||||||
Log.d(TAG, mRouteId + ": Selected");
|
Log.d(TAG, mRouteId + ": Selected");
|
||||||
|
mOverlay.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onUnselect() {
|
public void onUnselect() {
|
||||||
Log.d(TAG, mRouteId + ": Unselected");
|
Log.d(TAG, mRouteId + ": Unselected");
|
||||||
|
mMediaPlayer.onStop();
|
||||||
|
mOverlay.dismiss();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -192,6 +244,45 @@ final class SampleMediaRouteProvider extends MediaRouteProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onControlRequest(Intent intent, ControlRequestCallback callback) {
|
||||||
|
Log.d(TAG, mRouteId + ": Received control request " + intent);
|
||||||
|
String action = intent.getAction();
|
||||||
|
if (intent.hasCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)) {
|
||||||
|
boolean success = false;
|
||||||
|
if (action.equals(MediaControlIntent.ACTION_PLAY)) {
|
||||||
|
success = handlePlay(intent, callback);
|
||||||
|
} else if (action.equals(MediaControlIntent.ACTION_ENQUEUE)) {
|
||||||
|
success = handleEnqueue(intent, callback);
|
||||||
|
} else if (action.equals(MediaControlIntent.ACTION_REMOVE)) {
|
||||||
|
success = handleRemove(intent, callback);
|
||||||
|
} else if (action.equals(MediaControlIntent.ACTION_SEEK)) {
|
||||||
|
success = handleSeek(intent, callback);
|
||||||
|
} else if (action.equals(MediaControlIntent.ACTION_GET_STATUS)) {
|
||||||
|
success = handleGetStatus(intent, callback);
|
||||||
|
} else if (action.equals(MediaControlIntent.ACTION_PAUSE)) {
|
||||||
|
success = handlePause(intent, callback);
|
||||||
|
} else if (action.equals(MediaControlIntent.ACTION_RESUME)) {
|
||||||
|
success = handleResume(intent, callback);
|
||||||
|
} else if (action.equals(MediaControlIntent.ACTION_STOP)) {
|
||||||
|
success = handleStop(intent, callback);
|
||||||
|
}
|
||||||
|
Log.d(TAG, mSessionManager.toString());
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.equals(ACTION_GET_STATISTICS)
|
||||||
|
&& intent.hasCategory(CATEGORY_SAMPLE_ROUTE)) {
|
||||||
|
Bundle data = new Bundle();
|
||||||
|
data.putInt(DATA_PLAYBACK_COUNT, mEnqueueCount);
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onResult(data);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private void setVolumeInternal(int volume) {
|
private void setVolumeInternal(int volume) {
|
||||||
if (volume >= 0 && volume <= VOLUME_MAX) {
|
if (volume >= 0 && volume <= VOLUME_MAX) {
|
||||||
mVolume = volume;
|
mVolume = volume;
|
||||||
@@ -200,63 +291,198 @@ final class SampleMediaRouteProvider extends MediaRouteProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private boolean handlePlay(Intent intent, ControlRequestCallback callback) {
|
||||||
public boolean onControlRequest(Intent intent, ControlRequestCallback callback) {
|
String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
|
||||||
Log.d(TAG, mRouteId + ": Received control request " + intent);
|
if (sid == null || mSessionManager.stop(sid)) {
|
||||||
if (intent.getAction().equals(MediaControlIntent.ACTION_PLAY)
|
Log.d(TAG, "handleEnqueue");
|
||||||
&& intent.hasCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)
|
return handleEnqueue(intent, callback);
|
||||||
&& intent.getData() != null) {
|
|
||||||
mPlaybackCount +=1;
|
|
||||||
|
|
||||||
// TODO: Handle queue ids.
|
|
||||||
Uri uri = intent.getData();
|
|
||||||
long contentPositionMillis = intent.getLongExtra(
|
|
||||||
MediaControlIntent.EXTRA_ITEM_CONTENT_POSITION, 0);
|
|
||||||
Bundle metadata = intent.getBundleExtra(MediaControlIntent.EXTRA_ITEM_METADATA);
|
|
||||||
Bundle headers = intent.getBundleExtra(
|
|
||||||
MediaControlIntent.EXTRA_ITEM_HTTP_HEADERS);
|
|
||||||
|
|
||||||
Log.d(TAG, mRouteId + ": Received play request, uri=" + uri
|
|
||||||
+ ", contentPositionMillis=" + contentPositionMillis
|
|
||||||
+ ", metadata=" + metadata
|
|
||||||
+ ", headers=" + headers);
|
|
||||||
|
|
||||||
if (uri.toString().contains("hats")) {
|
|
||||||
// Simulate generating an error whenever the uri contains the word 'hats'.
|
|
||||||
Toast.makeText(getContext(), "Route rejected play request: uri=" + uri
|
|
||||||
+ ", no hats allowed!", Toast.LENGTH_LONG).show();
|
|
||||||
if (callback != null) {
|
|
||||||
callback.onError("Simulated error. No hats allowed!", null);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Toast.makeText(getContext(), "Route received play request: uri=" + uri,
|
|
||||||
Toast.LENGTH_LONG).show();
|
|
||||||
String streamId = generateStreamId();
|
|
||||||
if (callback != null) {
|
|
||||||
MediaItemStatus status = new MediaItemStatus.Builder(
|
|
||||||
MediaItemStatus.PLAYBACK_STATE_PLAYING)
|
|
||||||
.setContentPosition(contentPositionMillis)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
Bundle result = new Bundle();
|
|
||||||
result.putString(MediaControlIntent.EXTRA_ITEM_ID, streamId);
|
|
||||||
result.putBundle(MediaControlIntent.EXTRA_ITEM_STATUS, status.asBundle());
|
|
||||||
callback.onResult(result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (intent.getAction().equals(ACTION_GET_STATISTICS)
|
|
||||||
&& intent.hasCategory(CATEGORY_SAMPLE_ROUTE)) {
|
|
||||||
Bundle data = new Bundle();
|
|
||||||
data.putInt(DATA_PLAYBACK_COUNT, mPlaybackCount);
|
|
||||||
if (callback != null) {
|
|
||||||
callback.onResult(data);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean handleEnqueue(Intent intent, ControlRequestCallback callback) {
|
||||||
|
if (intent.getData() == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
mEnqueueCount +=1;
|
||||||
|
|
||||||
|
boolean enqueue = intent.getAction().equals(MediaControlIntent.ACTION_ENQUEUE);
|
||||||
|
Uri uri = intent.getData();
|
||||||
|
String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
|
||||||
|
long pos = intent.getLongExtra(MediaControlIntent.EXTRA_ITEM_CONTENT_POSITION, 0);
|
||||||
|
Bundle metadata = intent.getBundleExtra(MediaControlIntent.EXTRA_ITEM_METADATA);
|
||||||
|
Bundle headers = intent.getBundleExtra(MediaControlIntent.EXTRA_ITEM_HTTP_HEADERS);
|
||||||
|
PendingIntent receiver = (PendingIntent)intent.getParcelableExtra(
|
||||||
|
MediaControlIntent.EXTRA_ITEM_STATUS_UPDATE_RECEIVER);
|
||||||
|
|
||||||
|
Log.d(TAG, mRouteId + ": Received " + (enqueue?"enqueue":"play") + " request"
|
||||||
|
+ ", uri=" + uri
|
||||||
|
+ ", sid=" + sid
|
||||||
|
+ ", pos=" + pos
|
||||||
|
+ ", metadata=" + metadata
|
||||||
|
+ ", headers=" + headers
|
||||||
|
+ ", receiver=" + receiver);
|
||||||
|
MediaQueueItem item = mSessionManager.enqueue(sid, uri, receiver);
|
||||||
|
if (callback != null) {
|
||||||
|
if (item != null) {
|
||||||
|
Bundle result = new Bundle();
|
||||||
|
result.putString(MediaControlIntent.EXTRA_SESSION_ID, item.getSessionId());
|
||||||
|
result.putString(MediaControlIntent.EXTRA_ITEM_ID, item.getItemId());
|
||||||
|
result.putBundle(MediaControlIntent.EXTRA_ITEM_STATUS,
|
||||||
|
item.getStatus().asBundle());
|
||||||
|
callback.onResult(result);
|
||||||
|
} else {
|
||||||
|
callback.onError("Failed to open " + uri.toString(), null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean handleRemove(Intent intent, ControlRequestCallback callback) {
|
||||||
|
String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
|
||||||
|
String iid = intent.getStringExtra(MediaControlIntent.EXTRA_ITEM_ID);
|
||||||
|
MediaQueueItem item = mSessionManager.remove(sid, iid);
|
||||||
|
if (callback != null) {
|
||||||
|
if (item != null) {
|
||||||
|
Bundle result = new Bundle();
|
||||||
|
result.putBundle(MediaControlIntent.EXTRA_ITEM_STATUS,
|
||||||
|
item.getStatus().asBundle());
|
||||||
|
callback.onResult(result);
|
||||||
|
} else {
|
||||||
|
callback.onError("Failed to remove" +
|
||||||
|
", sid=" + sid + ", iid=" + iid, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (item != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean handleSeek(Intent intent, ControlRequestCallback callback) {
|
||||||
|
String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
|
||||||
|
String iid = intent.getStringExtra(MediaControlIntent.EXTRA_ITEM_ID);
|
||||||
|
long pos = intent.getLongExtra(MediaControlIntent.EXTRA_ITEM_CONTENT_POSITION, 0);
|
||||||
|
Log.d(TAG, mRouteId + ": Received seek request, pos=" + pos);
|
||||||
|
MediaQueueItem item = mSessionManager.seek(sid, iid, pos);
|
||||||
|
if (callback != null) {
|
||||||
|
if (item != null) {
|
||||||
|
Bundle result = new Bundle();
|
||||||
|
result.putBundle(MediaControlIntent.EXTRA_ITEM_STATUS,
|
||||||
|
item.getStatus().asBundle());
|
||||||
|
callback.onResult(result);
|
||||||
|
} else {
|
||||||
|
callback.onError("Failed to seek" +
|
||||||
|
", sid=" + sid + ", iid=" + iid + ", pos=" + pos, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (item != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean handleGetStatus(Intent intent, ControlRequestCallback callback) {
|
||||||
|
String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
|
||||||
|
String iid = intent.getStringExtra(MediaControlIntent.EXTRA_ITEM_ID);
|
||||||
|
MediaQueueItem item = mSessionManager.getStatus(sid, iid);
|
||||||
|
if (callback != null) {
|
||||||
|
if (item != null) {
|
||||||
|
Bundle result = new Bundle();
|
||||||
|
result.putBundle(MediaControlIntent.EXTRA_ITEM_STATUS,
|
||||||
|
item.getStatus().asBundle());
|
||||||
|
callback.onResult(result);
|
||||||
|
} else {
|
||||||
|
callback.onError("Failed to get status" +
|
||||||
|
", sid=" + sid + ", iid=" + iid, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (item != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean handlePause(Intent intent, ControlRequestCallback callback) {
|
||||||
|
String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
|
||||||
|
boolean success = mSessionManager.pause(sid);
|
||||||
|
if (callback != null) {
|
||||||
|
if (success) {
|
||||||
|
callback.onResult(null);
|
||||||
|
} else {
|
||||||
|
callback.onError("Failed to pause, sid=" + sid, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean handleResume(Intent intent, ControlRequestCallback callback) {
|
||||||
|
String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
|
||||||
|
boolean success = mSessionManager.resume(sid);
|
||||||
|
if (callback != null) {
|
||||||
|
if (success) {
|
||||||
|
callback.onResult(null);
|
||||||
|
} else {
|
||||||
|
callback.onError("Failed to resume, sid=" + sid, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean handleStop(Intent intent, ControlRequestCallback callback) {
|
||||||
|
String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
|
||||||
|
boolean success = mSessionManager.stop(sid);
|
||||||
|
if (callback != null) {
|
||||||
|
if (success) {
|
||||||
|
callback.onResult(null);
|
||||||
|
} else {
|
||||||
|
callback.onError("Failed to stop, sid=" + sid, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleFinish(boolean error) {
|
||||||
|
MediaQueueItem item = mSessionManager.finish(error);
|
||||||
|
if (item != null) {
|
||||||
|
handleStatusChange(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleStatusChange(MediaQueueItem item) {
|
||||||
|
if (item == null) {
|
||||||
|
item = mSessionManager.getCurrentItem();
|
||||||
|
}
|
||||||
|
if (item != null) {
|
||||||
|
PendingIntent receiver = item.getUpdateReceiver();
|
||||||
|
if (receiver != null) {
|
||||||
|
Intent intent = new Intent();
|
||||||
|
intent.putExtra(MediaControlIntent.EXTRA_SESSION_ID, item.getSessionId());
|
||||||
|
intent.putExtra(MediaControlIntent.EXTRA_ITEM_ID, item.getItemId());
|
||||||
|
intent.putExtra(MediaControlIntent.EXTRA_ITEM_STATUS,
|
||||||
|
item.getStatus().asBundle());
|
||||||
|
try {
|
||||||
|
receiver.send(getContext(), 0, intent);
|
||||||
|
Log.d(TAG, mRouteId + ": Sending status update from provider");
|
||||||
|
} catch (PendingIntent.CanceledException e) {
|
||||||
|
Log.d(TAG, mRouteId + ": Failed to send status update!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class MediaPlayerCallback extends MediaPlayerWrapper.Callback {
|
||||||
|
@Override
|
||||||
|
public void onError() {
|
||||||
|
handleFinish(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCompletion() {
|
||||||
|
handleFinish(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStatusChanged() {
|
||||||
|
handleStatusChange(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSizeChanged(int width, int height) {
|
||||||
|
mOverlay.updateAspectRatio(width, height);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||