summaryrefslogtreecommitdiffstats
path: root/tests/OneMedia
diff options
context:
space:
mode:
authorRoboErik <epastern@google.com>2014-02-13 14:22:42 -0800
committerRoboErik <epastern@google.com>2014-02-19 13:41:38 -0800
commitbfa153b64b4e8c2faa39a15e87fc9f0300335f20 (patch)
tree2170cb41d7a69560a0a2390528ea1319c2916e1d /tests/OneMedia
parent01fe661ae5da3739215d93922412df4b24c859a2 (diff)
downloadframeworks_base-bfa153b64b4e8c2faa39a15e87fc9f0300335f20.zip
frameworks_base-bfa153b64b4e8c2faa39a15e87fc9f0300335f20.tar.gz
frameworks_base-bfa153b64b4e8c2faa39a15e87fc9f0300335f20.tar.bz2
Initial commit for MediaSession test app
This app creates a service and UI in separate processes and uses the new MediaSession APIs. This is still a rough work in progress. Change-Id: I9692c95bf2fdbee7255da86dff59044c893e3a1f
Diffstat (limited to 'tests/OneMedia')
-rw-r--r--tests/OneMedia/Android.mk18
-rw-r--r--tests/OneMedia/AndroidManifest.xml30
-rw-r--r--tests/OneMedia/res/drawable-hdpi/ic_launcher.pngbin0 -> 7658 bytes
-rw-r--r--tests/OneMedia/res/drawable-mdpi/ic_launcher.pngbin0 -> 3777 bytes
-rw-r--r--tests/OneMedia/res/drawable-xhdpi/ic_launcher.pngbin0 -> 12516 bytes
-rw-r--r--tests/OneMedia/res/drawable-xxhdpi/ic_launcher.pngbin0 -> 24777 bytes
-rw-r--r--tests/OneMedia/res/layout/activity_main.xml16
-rw-r--r--tests/OneMedia/res/layout/activity_one_player.xml61
-rw-r--r--tests/OneMedia/res/menu/main.xml9
-rw-r--r--tests/OneMedia/res/values/colors.xml22
-rw-r--r--tests/OneMedia/res/values/dimens.xml9
-rw-r--r--tests/OneMedia/res/values/strings.xml15
-rw-r--r--tests/OneMedia/res/values/styles.xml44
-rw-r--r--tests/OneMedia/src/com/android/onemedia/IPlayerCallback.aidl22
-rw-r--r--tests/OneMedia/src/com/android/onemedia/IPlayerService.aidl29
-rw-r--r--tests/OneMedia/src/com/android/onemedia/OnePlayerActivity.java144
-rw-r--r--tests/OneMedia/src/com/android/onemedia/OnePlayerService.java30
-rw-r--r--tests/OneMedia/src/com/android/onemedia/PlayerController.java157
-rw-r--r--tests/OneMedia/src/com/android/onemedia/PlayerService.java102
-rw-r--r--tests/OneMedia/src/com/android/onemedia/PlayerSession.java117
-rw-r--r--tests/OneMedia/src/com/android/onemedia/playback/IRequestCallback.aidl22
-rw-r--r--tests/OneMedia/src/com/android/onemedia/playback/LocalRenderer.java703
-rw-r--r--tests/OneMedia/src/com/android/onemedia/playback/MediaItem.java59
-rw-r--r--tests/OneMedia/src/com/android/onemedia/playback/PlaybackError.java10
-rw-r--r--tests/OneMedia/src/com/android/onemedia/playback/Renderer.java199
-rw-r--r--tests/OneMedia/src/com/android/onemedia/playback/RendererFactory.java22
-rw-r--r--tests/OneMedia/src/com/android/onemedia/playback/RequestUtils.java53
27 files changed, 1893 insertions, 0 deletions
diff --git a/tests/OneMedia/Android.mk b/tests/OneMedia/Android.mk
new file mode 100644
index 0000000..93b9c9a
--- /dev/null
+++ b/tests/OneMedia/Android.mk
@@ -0,0 +1,18 @@
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_SRC_FILES := $(call all-subdir-java-files) \
+ $(call all-Iaidl-files-under, src)
+
+LOCAL_PACKAGE_NAME := OneMedia
+LOCAL_CERTIFICATE := platform
+
+LOCAL_STATIC_JAVA_LIBRARIES := \
+ android-support-v7-appcompat \
+ android-support-v7-mediarouter
+
+LOCAL_PROGUARD_ENABLED := disabled
+
+include $(BUILD_PACKAGE)
diff --git a/tests/OneMedia/AndroidManifest.xml b/tests/OneMedia/AndroidManifest.xml
new file mode 100644
index 0000000..7d6ba1d
--- /dev/null
+++ b/tests/OneMedia/AndroidManifest.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.onemedia"
+ android:versionCode="1"
+ android:versionName="1.0" >
+
+ <uses-sdk android:minSdkVersion="19"/>
+ <uses-permission android:name="android.permission.INTERNET" />
+ <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+
+ <application
+ android:allowBackup="true"
+ android:icon="@drawable/ic_launcher"
+ android:label="@string/app_name"
+ android:theme="@style/AppTheme" >
+ <activity
+ android:name="com.android.onemedia.OnePlayerActivity"
+ android:label="@string/app_name" >
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ <service
+ android:name="com.android.onemedia.OnePlayerService"
+ android:exported="false"
+ android:process="com.android.onemedia.service" />
+ </application>
+
+</manifest>
diff --git a/tests/OneMedia/res/drawable-hdpi/ic_launcher.png b/tests/OneMedia/res/drawable-hdpi/ic_launcher.png
new file mode 100644
index 0000000..288b665
--- /dev/null
+++ b/tests/OneMedia/res/drawable-hdpi/ic_launcher.png
Binary files differ
diff --git a/tests/OneMedia/res/drawable-mdpi/ic_launcher.png b/tests/OneMedia/res/drawable-mdpi/ic_launcher.png
new file mode 100644
index 0000000..6ae570b
--- /dev/null
+++ b/tests/OneMedia/res/drawable-mdpi/ic_launcher.png
Binary files differ
diff --git a/tests/OneMedia/res/drawable-xhdpi/ic_launcher.png b/tests/OneMedia/res/drawable-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..d4fb7cd
--- /dev/null
+++ b/tests/OneMedia/res/drawable-xhdpi/ic_launcher.png
Binary files differ
diff --git a/tests/OneMedia/res/drawable-xxhdpi/ic_launcher.png b/tests/OneMedia/res/drawable-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..85a6081
--- /dev/null
+++ b/tests/OneMedia/res/drawable-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/tests/OneMedia/res/layout/activity_main.xml b/tests/OneMedia/res/layout/activity_main.xml
new file mode 100644
index 0000000..168c9b8
--- /dev/null
+++ b/tests/OneMedia/res/layout/activity_main.xml
@@ -0,0 +1,16 @@
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingBottom="@dimen/activity_vertical_margin"
+ android:paddingLeft="@dimen/activity_horizontal_margin"
+ android:paddingRight="@dimen/activity_horizontal_margin"
+ android:paddingTop="@dimen/activity_vertical_margin"
+ tools:context=".MainActivity" >
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/hello_world" />
+
+</RelativeLayout>
diff --git a/tests/OneMedia/res/layout/activity_one_player.xml b/tests/OneMedia/res/layout/activity_one_player.xml
new file mode 100644
index 0000000..4208355
--- /dev/null
+++ b/tests/OneMedia/res/layout/activity_one_player.xml
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2014 Google Inc. All Rights Reserved. -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center_horizontal"
+ android:orientation="vertical">
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:text="@string/app_name"
+ style="@style/Title" />
+ <EditText
+ android:id="@+id/content"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:inputType="textUri"
+ android:hint="@string/media_content_hint"
+ android:gravity="center"
+ android:textSize="24sp" />
+ <EditText
+ android:id="@+id/next_content"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:inputType="textNoSuggestions"
+ android:hint="@string/media_next_hint"
+ android:gravity="center"
+ android:textSize="24sp" />
+ <CheckBox
+ android:id="@+id/has_video"
+ android:layout_marginRight="8dip"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/has_video" />
+ <LinearLayout
+ android:id="@+id/controls"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal" >
+ <Button
+ android:id="@+id/start_button"
+ style="@style/BottomBarButton"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="@string/start_button" />
+ <Button
+ android:id="@+id/play_button"
+ style="@style/BottomBarButton"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="@string/play_button" />
+ </LinearLayout>
+ <TextView
+ android:id="@+id/status"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+
+</LinearLayout>
diff --git a/tests/OneMedia/res/menu/main.xml b/tests/OneMedia/res/menu/main.xml
new file mode 100644
index 0000000..c002028
--- /dev/null
+++ b/tests/OneMedia/res/menu/main.xml
@@ -0,0 +1,9 @@
+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <item
+ android:id="@+id/action_settings"
+ android:orderInCategory="100"
+ android:showAsAction="never"
+ android:title="@string/action_settings"/>
+
+</menu>
diff --git a/tests/OneMedia/res/values/colors.xml b/tests/OneMedia/res/values/colors.xml
new file mode 100644
index 0000000..9b9dc2a
--- /dev/null
+++ b/tests/OneMedia/res/values/colors.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2014 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+-->
+
+<resources>
+ <color name="title_color">#33B5E5</color>
+</resources>
diff --git a/tests/OneMedia/res/values/dimens.xml b/tests/OneMedia/res/values/dimens.xml
new file mode 100644
index 0000000..562edef
--- /dev/null
+++ b/tests/OneMedia/res/values/dimens.xml
@@ -0,0 +1,9 @@
+<resources>
+
+ <!-- Default screen margins, per the Android Design guidelines. -->
+ <dimen name="activity_horizontal_margin">16dp</dimen>
+ <dimen name="activity_vertical_margin">16dp</dimen>
+ <dimen name="title_size">22sp</dimen>
+ <dimen name="small_size">11sp</dimen>
+
+</resources>
diff --git a/tests/OneMedia/res/values/strings.xml b/tests/OneMedia/res/values/strings.xml
new file mode 100644
index 0000000..1b0cebb
--- /dev/null
+++ b/tests/OneMedia/res/values/strings.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="app_name">OneMedia</string>
+ <string name="action_settings">Settings</string>
+ <string name="hello_world">Test app for trying out new media components</string>
+
+ <string name="start_button">Start</string>
+ <string name="play_button">Play</string>
+ <string name="media_content_hint">Content</string>
+ <string name="media_next_hint">Next content</string>
+ <string name="has_video">Is video</string>
+ <string name="has_duration">Has duration</string>
+
+</resources>
diff --git a/tests/OneMedia/res/values/styles.xml b/tests/OneMedia/res/values/styles.xml
new file mode 100644
index 0000000..60f3139
--- /dev/null
+++ b/tests/OneMedia/res/values/styles.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!--
+ Base application theme, dependent on API level. This theme is replaced
+ by AppBaseTheme from res/values-vXX/styles.xml on newer devices.
+ -->
+ <style name="AppBaseTheme" parent="android:Theme.Light">
+ <!--
+ Theme customizations available in newer API levels can go in
+ res/values-vXX/styles.xml, while customizations related to
+ backward-compatibility can go here.
+ -->
+ </style>
+
+ <!-- Application theme. -->
+ <style name="AppTheme" parent="AppBaseTheme">
+ <!-- All customizations that are NOT specific to a particular API-level can go here. -->
+ </style>
+
+ <style name="Title">
+ <item name="android:textSize">@dimen/title_size</item>
+ <item name="android:textColor">@color/title_color</item>
+ <item name="android:clickable">false</item>
+ <item name="android:longClickable">false</item>
+ </style>
+
+ <style name="Text">
+ <item name="android:textSize">@dimen/small_size</item>
+ <item name="android:textColor">@color/title_color</item>
+ <item name="android:clickable">false</item>
+ <item name="android:longClickable">false</item>
+ </style>
+
+ <style name="BottomBarButton">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">match_parent</item>
+ <item name="android:paddingTop">0dip</item>
+ <item name="android:paddingLeft">0dip</item>
+ <item name="android:paddingRight">0dip</item>
+ <item name="android:paddingBottom">0dip</item>
+ <item name="android:textSize">12sp</item>
+ <item name="android:textStyle">bold</item>
+ </style>
+</resources>
diff --git a/tests/OneMedia/src/com/android/onemedia/IPlayerCallback.aidl b/tests/OneMedia/src/com/android/onemedia/IPlayerCallback.aidl
new file mode 100644
index 0000000..9bc3baa
--- /dev/null
+++ b/tests/OneMedia/src/com/android/onemedia/IPlayerCallback.aidl
@@ -0,0 +1,22 @@
+/* Copyright (C) 2014 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.android.onemedia;
+
+import android.media.MediaSessionToken;
+
+interface IPlayerCallback {
+ void onSessionChanged(in MediaSessionToken session);
+} \ No newline at end of file
diff --git a/tests/OneMedia/src/com/android/onemedia/IPlayerService.aidl b/tests/OneMedia/src/com/android/onemedia/IPlayerService.aidl
new file mode 100644
index 0000000..ab1d3fc
--- /dev/null
+++ b/tests/OneMedia/src/com/android/onemedia/IPlayerService.aidl
@@ -0,0 +1,29 @@
+/* Copyright (C) 2014 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.android.onemedia;
+
+import android.media.MediaSessionToken;
+import android.os.Bundle;
+
+import com.android.onemedia.IPlayerCallback;
+import com.android.onemedia.playback.IRequestCallback;
+
+interface IPlayerService {
+ MediaSessionToken getSessionToken();
+ void registerCallback(in IPlayerCallback cb);
+ void unregisterCallback(in IPlayerCallback cb);
+ void sendRequest(String action, in Bundle params, in IRequestCallback cb);
+} \ No newline at end of file
diff --git a/tests/OneMedia/src/com/android/onemedia/OnePlayerActivity.java b/tests/OneMedia/src/com/android/onemedia/OnePlayerActivity.java
new file mode 100644
index 0000000..7ff81e4
--- /dev/null
+++ b/tests/OneMedia/src/com/android/onemedia/OnePlayerActivity.java
@@ -0,0 +1,144 @@
+package com.android.onemedia;
+
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.Menu;
+import android.view.View;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.EditText;
+import android.widget.TextView;
+
+import com.android.onemedia.playback.Renderer;
+
+public class OnePlayerActivity extends Activity {
+ private static final String TAG = "OnePlayerActivity";
+
+ protected PlayerController mPlayer;
+
+ private Button mStartButton;
+ private Button mPlayButton;
+ private TextView mStatusView;
+
+ private EditText mContentText;
+ private EditText mNextContentText;
+ private CheckBox mHasVideo;
+
+ private int mPlaybackState;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_one_player);
+ mPlayer = new PlayerController(this, OnePlayerService.getServiceIntent(this));
+
+
+ mStartButton = (Button) findViewById(R.id.start_button);
+ mPlayButton = (Button) findViewById(R.id.play_button);
+ mStatusView = (TextView) findViewById(R.id.status);
+ mContentText = (EditText) findViewById(R.id.content);
+ mNextContentText = (EditText) findViewById(R.id.next_content);
+ mHasVideo = (CheckBox) findViewById(R.id.has_video);
+
+ mStartButton.setOnClickListener(mButtonListener);
+ mPlayButton.setOnClickListener(mButtonListener);
+
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ // Inflate the menu; this adds items to the action bar if it is present.
+ getMenuInflater().inflate(R.menu.main, menu);
+ return true;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ mPlayer.onResume();
+ mPlayer.setListener(mListener);
+ }
+
+ @Override
+ public void onPause() {
+ mPlayer.setListener(null);
+ mPlayer.onPause();
+ super.onPause();
+ }
+
+ private void setControlsEnabled(boolean enabled) {
+ mStartButton.setEnabled(enabled);
+ mPlayButton.setEnabled(enabled);
+ }
+
+ private View.OnClickListener mButtonListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ switch (v.getId()) {
+ case R.id.play_button:
+ Log.d(TAG, "Play button pressed, in state " + mPlaybackState);
+ if (mPlaybackState == Renderer.STATE_PAUSED
+ || mPlaybackState == Renderer.STATE_ENDED) {
+ mPlayer.play();
+ } else if (mPlaybackState == Renderer.STATE_PLAYING) {
+ mPlayer.pause();
+ }
+ break;
+ case R.id.start_button:
+ Log.d(TAG, "Start button pressed, in state " + mPlaybackState);
+ mPlayer.setContent(mContentText.getText().toString());
+ break;
+ }
+
+ }
+ };
+
+ private PlayerController.Listener mListener = new PlayerController.Listener() {
+ @Override
+ public void onSessionStateChange(int state) {
+ mPlaybackState = state;
+ boolean enablePlay = false;
+ switch (mPlaybackState) {
+ case Renderer.STATE_PLAYING:
+ mStatusView.setText("playing");
+ mPlayButton.setText("Pause");
+ enablePlay = true;
+ break;
+ case Renderer.STATE_PAUSED:
+ mStatusView.setText("paused");
+ mPlayButton.setText("Play");
+ enablePlay = true;
+ break;
+ case Renderer.STATE_ENDED:
+ mStatusView.setText("ended");
+ mPlayButton.setText("Play");
+ enablePlay = true;
+ break;
+ case Renderer.STATE_ERROR:
+ mStatusView.setText("error");
+ break;
+ case Renderer.STATE_PREPARING:
+ mStatusView.setText("preparing");
+ break;
+ case Renderer.STATE_READY:
+ mStatusView.setText("ready");
+ break;
+ case Renderer.STATE_STOPPED:
+ mStatusView.setText("stopped");
+ break;
+ }
+ mPlayButton.setEnabled(enablePlay);
+ }
+
+ @Override
+ public void onPlayerStateChange(int state) {
+ if (state == PlayerController.STATE_DISCONNECTED) {
+ setControlsEnabled(false);
+ } else if (state == PlayerController.STATE_CONNECTED) {
+ setControlsEnabled(true);
+ }
+ }
+ };
+}
diff --git a/tests/OneMedia/src/com/android/onemedia/OnePlayerService.java b/tests/OneMedia/src/com/android/onemedia/OnePlayerService.java
new file mode 100644
index 0000000..01610cd
--- /dev/null
+++ b/tests/OneMedia/src/com/android/onemedia/OnePlayerService.java
@@ -0,0 +1,30 @@
+package com.android.onemedia;
+
+import android.content.Context;
+import android.content.Intent;
+
+import java.util.ArrayList;
+
+/**
+ * TODO: Insert description here. (generated by epastern)
+ */
+public class OnePlayerService extends PlayerService {
+ private static final String TAG = "OnePlayerService";
+
+ public static Intent getServiceIntent(Context context) {
+ return new Intent(context, OnePlayerService.class).setPackage(
+ OnePlayerService.class.getPackage().getName());
+ }
+
+ @Override
+ protected Intent onCreateServiceIntent() {
+ return getServiceIntent(this);
+ }
+
+ @Override
+ protected ArrayList<String> getAllowedPackages() {
+ ArrayList<String> allowedPackages = new ArrayList<String>();
+ allowedPackages.add("com.android.onemedia");
+ return allowedPackages;
+ }
+}
diff --git a/tests/OneMedia/src/com/android/onemedia/PlayerController.java b/tests/OneMedia/src/com/android/onemedia/PlayerController.java
new file mode 100644
index 0000000..4ccc846
--- /dev/null
+++ b/tests/OneMedia/src/com/android/onemedia/PlayerController.java
@@ -0,0 +1,157 @@
+
+package com.android.onemedia;
+
+import android.media.MediaController;
+import android.media.MediaSessionManager;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.util.Log;
+import android.view.KeyEvent;
+
+import com.android.onemedia.playback.RequestUtils;
+
+public class PlayerController {
+ private static final String TAG = "PlayerSession";
+
+ public static final int STATE_DISCONNECTED = 0;
+ public static final int STATE_CONNECTED = 1;
+
+ protected MediaController mController;
+ protected IPlayerService mBinder;
+
+ private final Intent mServiceIntent;
+ private Context mContext;
+ private Listener mListener;
+ private SessionCallback mControllerCb;
+ private MediaSessionManager mManager;
+ private Handler mHandler = new Handler();
+
+ private boolean mResumed;
+
+ public PlayerController(Context context, Intent serviceIntent) {
+ mContext = context;
+ if (serviceIntent == null) {
+ mServiceIntent = new Intent(mContext, PlayerService.class);
+ } else {
+ mServiceIntent = serviceIntent;
+ }
+ mControllerCb = new SessionCallback();
+ mManager = (MediaSessionManager) context
+ .getSystemService(Context.MEDIA_SESSION_SERVICE);
+
+ mResumed = false;
+ }
+
+ public void setListener(Listener listener) {
+ mListener = listener;
+ Log.d(TAG, "Listener set to " + listener + " session is " + mController);
+ if (mListener != null) {
+ mHandler = new Handler();
+ mListener.onPlayerStateChange(
+ mController == null ? STATE_DISCONNECTED : STATE_CONNECTED);
+ }
+ }
+
+ public void onResume() {
+ mResumed = true;
+ Log.d(TAG, "onResume. Binding to service with intent " + mServiceIntent.toString());
+ bindToService();
+ }
+
+ public void onPause() {
+ mResumed = false;
+ Log.d(TAG, "onPause, unbinding from service");
+ unbindFromService();
+ }
+
+ public void play() {
+ mController.sendMediaButton(KeyEvent.KEYCODE_MEDIA_PLAY);
+ }
+
+ public void pause() {
+ mController.sendMediaButton(KeyEvent.KEYCODE_MEDIA_PAUSE);
+ }
+
+ public void setContent(String source) {
+ RequestUtils.ContentBuilder bob = new RequestUtils.ContentBuilder();
+ bob.setSource(source);
+ try {
+ mBinder.sendRequest(RequestUtils.ACTION_SET_CONTENT, bob.build(), null);
+ } catch (RemoteException e) {
+ Log.d(TAG, "setContent failed, service may have died.", e);
+ }
+ }
+
+ public void setNextContent(String source) {
+ RequestUtils.ContentBuilder bob = new RequestUtils.ContentBuilder();
+ bob.setSource(source);
+ try {
+ mBinder.sendRequest(RequestUtils.ACTION_SET_NEXT_CONTENT, bob.build(), null);
+ } catch (RemoteException e) {
+ Log.d(TAG, "setNexctContent failed, service may have died.", e);
+ }
+ }
+
+ private void unbindFromService() {
+ mContext.unbindService(mServiceConnection);
+ }
+
+ private void bindToService() {
+ mContext.bindService(mServiceIntent, mServiceConnection, Context.BIND_AUTO_CREATE);
+ }
+
+ private ServiceConnection mServiceConnection = new ServiceConnection() {
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ if (mController != null) {
+ mController.removeCallback(mControllerCb);
+ }
+ mBinder = null;
+ mController = null;
+ Log.d(TAG, "Disconnected from PlayerService");
+
+ if (mListener != null) {
+ mListener.onPlayerStateChange(STATE_DISCONNECTED);
+ }
+ }
+
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ mBinder = IPlayerService.Stub.asInterface(service);
+ Log.d(TAG, "service is " + service + " binder is " + mBinder);
+ try {
+ mController = new MediaController(mBinder.getSessionToken());
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error getting session", e);
+ return;
+ }
+ mController.addCallback(mControllerCb, mHandler);
+ Log.d(TAG, "Ready to use PlayerService");
+
+ if (mListener != null) {
+ mListener.onPlayerStateChange(STATE_CONNECTED);
+ }
+ }
+ };
+
+ private class SessionCallback extends MediaController.Callback {
+ @Override
+ public void onPlaybackStateChange(int state) {
+ if (mListener != null) {
+ mListener.onSessionStateChange(state);
+ }
+ }
+ }
+
+ public interface Listener {
+ public void onSessionStateChange(int state);
+
+ public void onPlayerStateChange(int state);
+ }
+
+}
diff --git a/tests/OneMedia/src/com/android/onemedia/PlayerService.java b/tests/OneMedia/src/com/android/onemedia/PlayerService.java
new file mode 100644
index 0000000..0819077
--- /dev/null
+++ b/tests/OneMedia/src/com/android/onemedia/PlayerService.java
@@ -0,0 +1,102 @@
+package com.android.onemedia;
+
+import android.app.Service;
+import android.content.Intent;
+import android.media.MediaSessionToken;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+
+import com.android.onemedia.playback.IRequestCallback;
+import com.android.onemedia.playback.RequestUtils;
+
+import java.util.ArrayList;
+
+public class PlayerService extends Service {
+ private static final String TAG = "PlayerService";
+
+ private PlayerBinder mBinder;
+ private PlayerSession mSession;
+ private Intent mIntent;
+
+ private ArrayList<IPlayerCallback> mCbs = new ArrayList<IPlayerCallback>();
+
+ @Override
+ public void onCreate() {
+ mIntent = onCreateServiceIntent();
+ mSession = onCreatePlayerController();
+ mSession.createSession();
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ if (mBinder == null) {
+ mBinder = new PlayerBinder();
+ }
+ return mBinder;
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ return START_STICKY;
+ }
+
+ @Override
+ public void onDestroy() {
+ mSession.onDestroy();
+ }
+
+ protected Intent onCreateServiceIntent() {
+ return new Intent(this, PlayerService.class).setPackage(getBasePackageName());
+ }
+
+ protected PlayerSession onCreatePlayerController() {
+ return new PlayerSession(this);
+ }
+
+ protected ArrayList<String> getAllowedPackages() {
+ return null;
+ }
+
+ public class PlayerBinder extends IPlayerService.Stub {
+ @Override
+ public void sendRequest(String action, Bundle params, IRequestCallback cb) {
+ if (RequestUtils.ACTION_SET_CONTENT.equals(action)) {
+ mSession.setContent(params);
+ } else if (RequestUtils.ACTION_SET_NEXT_CONTENT.equals(action)) {
+ mSession.setNextContent(params);
+ }
+ }
+
+ @Override
+ public void registerCallback(final IPlayerCallback cb) throws RemoteException {
+ if (!mCbs.contains(cb)) {
+ mCbs.add(cb);
+ cb.asBinder().linkToDeath(new IBinder.DeathRecipient() {
+ @Override
+ public void binderDied() {
+ mCbs.remove(cb);
+ }
+ }, 0);
+ }
+ try {
+ cb.onSessionChanged(getSessionToken());
+ } catch (RemoteException e) {
+ mCbs.remove(cb);
+ throw e;
+ }
+ }
+
+ @Override
+ public void unregisterCallback(IPlayerCallback cb) throws RemoteException {
+ mCbs.remove(cb);
+ }
+
+ @Override
+ public MediaSessionToken getSessionToken() throws RemoteException {
+ // TODO(epastern): Auto-generated method stub
+ return mSession.getSessionToken();
+ }
+ }
+
+}
diff --git a/tests/OneMedia/src/com/android/onemedia/PlayerSession.java b/tests/OneMedia/src/com/android/onemedia/PlayerSession.java
new file mode 100644
index 0000000..25a8f0d
--- /dev/null
+++ b/tests/OneMedia/src/com/android/onemedia/PlayerSession.java
@@ -0,0 +1,117 @@
+package com.android.onemedia;
+
+import android.content.Context;
+import android.content.Intent;
+import android.media.MediaSession;
+import android.media.MediaSessionManager;
+import android.media.MediaSessionToken;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.KeyEvent;
+
+import com.android.onemedia.playback.LocalRenderer;
+import com.android.onemedia.playback.Renderer;
+import com.android.onemedia.playback.RendererFactory;
+
+public class PlayerSession {
+ private static final String TAG = "PlayerController";
+
+ protected MediaSession mSession;
+ protected Context mContext;
+ protected RendererFactory mRendererFactory;
+ protected LocalRenderer mRenderer;
+ protected ControllerCb mCallback;
+ protected RenderListener mRenderListener;
+
+ public PlayerSession(Context context) {
+ mContext = context;
+ mRendererFactory = new RendererFactory();
+ mRenderer = new LocalRenderer(context, null);
+ mCallback = new ControllerCb();
+ mRenderListener = new RenderListener();
+
+ mRenderer.registerListener(mRenderListener);
+ }
+
+ public void createSession() {
+ if (mSession != null) {
+ mSession.release();
+ }
+ MediaSessionManager man = (MediaSessionManager) mContext
+ .getSystemService(Context.MEDIA_SESSION_SERVICE);
+ Log.d(TAG, "Creating session for package " + mContext.getBasePackageName());
+ mSession = man.createSession("OneMedia");
+ mSession.addCallback(mCallback);
+ }
+
+ public void onDestroy() {
+ if (mSession != null) {
+ mSession.release();
+ }
+ if (mRenderer != null) {
+ mRenderer.unregisterListener(mRenderListener);
+ mRenderer.onDestroy();
+ }
+ }
+
+ public MediaSessionToken getSessionToken() {
+ return mSession.getSessionToken();
+ }
+
+ public void setContent(Bundle request) {
+ mRenderer.setContent(request);
+ }
+
+ public void setNextContent(Bundle request) {
+ mRenderer.setNextContent(request);
+ }
+
+ protected class RenderListener implements Renderer.Listener {
+
+ @Override
+ public void onError(int type, int extra, Bundle extras, Throwable error) {
+ mSession.setPlaybackState(Renderer.STATE_ERROR);
+ }
+
+ @Override
+ public void onStateChanged(int newState) {
+ mSession.setPlaybackState(newState);
+ }
+
+ @Override
+ public void onBufferingUpdate(int percent) {
+ }
+
+ @Override
+ public void onFocusLost() {
+ mSession.setPlaybackState(Renderer.STATE_PAUSED);
+ }
+
+ @Override
+ public void onNextStarted() {
+ }
+
+ }
+
+ protected class ControllerCb extends MediaSession.Callback {
+
+ @Override
+ public void onMediaButton(Intent mediaRequestIntent) {
+ if (Intent.ACTION_MEDIA_BUTTON.equals(mediaRequestIntent.getAction())) {
+ KeyEvent event = (KeyEvent) mediaRequestIntent
+ .getParcelableExtra(Intent.EXTRA_KEY_EVENT);
+ switch (event.getKeyCode()) {
+ case KeyEvent.KEYCODE_MEDIA_PLAY:
+ Log.d(TAG, "play button received");
+ mRenderer.onPlay();
+ break;
+ case KeyEvent.KEYCODE_MEDIA_PAUSE:
+ Log.d(TAG, "pause button received");
+ mRenderer.onPause();
+ break;
+ }
+ }
+ }
+ }
+
+}
diff --git a/tests/OneMedia/src/com/android/onemedia/playback/IRequestCallback.aidl b/tests/OneMedia/src/com/android/onemedia/playback/IRequestCallback.aidl
new file mode 100644
index 0000000..c5a30a8
--- /dev/null
+++ b/tests/OneMedia/src/com/android/onemedia/playback/IRequestCallback.aidl
@@ -0,0 +1,22 @@
+/* Copyright (C) 2014 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.android.onemedia.playback;
+
+import android.os.Bundle;
+
+oneway interface IRequestCallback {
+ void onResult(in Bundle result);
+} \ No newline at end of file
diff --git a/tests/OneMedia/src/com/android/onemedia/playback/LocalRenderer.java b/tests/OneMedia/src/com/android/onemedia/playback/LocalRenderer.java
new file mode 100644
index 0000000..7493366
--- /dev/null
+++ b/tests/OneMedia/src/com/android/onemedia/playback/LocalRenderer.java
@@ -0,0 +1,703 @@
+package com.android.onemedia.playback;
+
+import org.apache.http.Header;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.methods.HttpGet;
+
+import android.content.Context;
+import android.media.AudioManager;
+import android.media.AudioManager.OnAudioFocusChangeListener;
+import android.media.MediaPlayer;
+import android.media.MediaPlayer.OnBufferingUpdateListener;
+import android.media.MediaPlayer.OnCompletionListener;
+import android.media.MediaPlayer.OnErrorListener;
+import android.media.MediaPlayer.OnPreparedListener;
+import android.net.Uri;
+import android.net.http.AndroidHttpClient;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.Handler;
+import android.util.Log;
+import android.view.SurfaceHolder;
+
+import java.io.IOException;
+import java.util.Map;
+
+/**
+ * Helper class for wrapping a MediaPlayer and doing a lot of the default work
+ * to play audio. This class is not currently thread safe and all calls to it
+ * should be made on the same thread.
+ */
+public class LocalRenderer extends Renderer implements OnPreparedListener,
+ OnBufferingUpdateListener, OnCompletionListener, OnErrorListener,
+ OnAudioFocusChangeListener {
+ private static final String TAG = "MediaPlayerManager";
+ private static final boolean DEBUG = true;
+ private static long sDebugInstanceId = 0;
+
+ private static final String[] SUPPORTED_FEATURES = {
+ FEATURE_SET_CONTENT,
+ FEATURE_SET_NEXT_CONTENT,
+ FEATURE_PLAY,
+ FEATURE_PAUSE,
+ FEATURE_NEXT,
+ FEATURE_PREVIOUS,
+ FEATURE_SEEK_TO,
+ FEATURE_STOP
+ };
+
+ /**
+ * These are the states where it is valid to call play directly on the
+ * MediaPlayer.
+ */
+ private static final int CAN_PLAY = STATE_READY | STATE_PAUSED | STATE_ENDED;
+ /**
+ * These are the states where we expect the MediaPlayer to be ready in the
+ * future, so we can set a flag to start playing when it is.
+ */
+ private static final int CAN_READY_PLAY = STATE_INIT | STATE_PREPARING;
+ /**
+ * The states when it is valid to call pause on the MediaPlayer.
+ */
+ private static final int CAN_PAUSE = STATE_PLAYING;
+ /**
+ * The states where it is valid to call seek on the MediaPlayer.
+ */
+ private static final int CAN_SEEK = STATE_READY | STATE_PLAYING | STATE_PAUSED | STATE_ENDED;
+ /**
+ * The states where we expect the MediaPlayer to be ready in the future and
+ * can store a seek position to set later.
+ */
+ private static final int CAN_READY_SEEK = STATE_INIT | STATE_PREPARING;
+ /**
+ * The states where it is valid to call stop on the MediaPlayer.
+ */
+ private static final int CAN_STOP = STATE_READY | STATE_PLAYING | STATE_PAUSED | STATE_ENDED;
+ /**
+ * The states where it is valid to get the current play position and the
+ * duration from the MediaPlayer.
+ */
+ private static final int CAN_GET_POSITION = STATE_READY | STATE_PLAYING | STATE_PAUSED;
+
+
+
+ private class PlayerContent {
+ public final String source;
+ public final Map<String, String> headers;
+
+ public PlayerContent(String source, Map<String, String> headers) {
+ this.source = source;
+ this.headers = headers;
+ }
+ }
+
+ private class AsyncErrorRetriever extends AsyncTask<HttpGet, Void, Void> {
+ private final long errorId;
+ private boolean closeHttpClient;
+
+ public AsyncErrorRetriever(long errorId) {
+ this.errorId = errorId;
+ closeHttpClient = false;
+ }
+
+ public boolean cancelRequestLocked(boolean closeHttp) {
+ closeHttpClient = closeHttp;
+ return this.cancel(false);
+ }
+
+ @Override
+ protected Void doInBackground(HttpGet[] params) {
+ synchronized (mErrorLock) {
+ if (isCancelled() || mHttpClient == null) {
+ if (mErrorRetriever == this) {
+ mErrorRetriever = null;
+ }
+ return null;
+ }
+ mSafeToCloseClient = false;
+ }
+ final PlaybackError error = new PlaybackError();
+ try {
+ HttpResponse response = mHttpClient.execute(params[0]);
+ synchronized (mErrorLock) {
+ if (mErrorId != errorId || mError == null) {
+ // A new error has occurred, abort
+ return null;
+ }
+ error.type = mError.type;
+ error.extra = mError.extra;
+ error.errorMessage = mError.errorMessage;
+ }
+ final int code = response.getStatusLine().getStatusCode();
+ if (code >= 300) {
+ error.extra = code;
+ }
+ final Bundle errorExtras = new Bundle();
+ Header[] headers = response.getAllHeaders();
+ if (headers != null && headers.length > 0) {
+ for (Header header : headers) {
+ errorExtras.putString(header.getName(), header.getValue());
+ }
+ error.errorExtras = errorExtras;
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "IOException requesting from server, unable to get more exact error");
+ } finally {
+ synchronized (mErrorLock) {
+ mSafeToCloseClient = true;
+ if (mErrorRetriever == this) {
+ mErrorRetriever = null;
+ }
+ if (isCancelled()) {
+ if (closeHttpClient) {
+ mHttpClient.close();
+ mHttpClient = null;
+ }
+ return null;
+ }
+ }
+ }
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ synchronized (mErrorLock) {
+ if (mErrorId == errorId) {
+ setError(error.type, error.extra, error.errorExtras, null);
+ }
+ }
+ }
+ });
+ return null;
+ }
+ }
+
+ private int mState = STATE_INIT;
+
+ private AudioManager mAudioManager;
+ private MediaPlayer mPlayer;
+ private PlayerContent mContent;
+ private MediaPlayer mNextPlayer;
+ private PlayerContent mNextContent;
+ private SurfaceHolder mHolder;
+ private SurfaceHolder.Callback mHolderCB;
+ private Context mContext;
+
+ private Handler mHandler = new Handler();
+
+ private AndroidHttpClient mHttpClient = AndroidHttpClient.newInstance("TUQ");
+ // The ongoing error request thread if there is one. This should only be
+ // modified while mErrorLock is held.
+ private AsyncErrorRetriever mErrorRetriever;
+ // This is set to false while a server request is being made to retrieve
+ // the current error. It should only be set while mErrorLock is held.
+ private boolean mSafeToCloseClient = true;
+ private final Object mErrorLock = new Object();
+ // A tracking id for the current error. This should only be modified while
+ // mErrorLock is held.
+ private long mErrorId = 0;
+ // The current error state of this player. This is cleared when the state
+ // leaves an error state and set when it enters one. This should only be
+ // modified when mErrorLock is held.
+ private PlaybackError mError;
+
+ private boolean mPlayOnReady;
+ private int mSeekOnReady;
+ private boolean mHasAudioFocus;
+ private long mDebugId = sDebugInstanceId++;
+
+ public LocalRenderer(Context context, Bundle params) {
+ super(context, params);
+ mContext = context;
+ mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ }
+
+ @Override
+ protected void initFeatures(Bundle params) {
+ for (String feature : SUPPORTED_FEATURES) {
+ mFeatures.add(feature);
+ }
+ }
+
+ /**
+ * Call this when completely finished with the MediaPlayerManager to have it
+ * clean up. The instance may not be used again after this is called.
+ */
+ @Override
+ public void onDestroy() {
+ synchronized (mErrorLock) {
+ if (DEBUG) {
+ Log.d(TAG, "onDestroy, error retriever? " + mErrorRetriever + " safe to close? "
+ + mSafeToCloseClient + " client? " + mHttpClient);
+ }
+ if (mErrorRetriever != null) {
+ mErrorRetriever.cancelRequestLocked(true);
+ mErrorRetriever = null;
+ }
+ // Increment the error id to ensure no errors are sent after this
+ // point.
+ mErrorId++;
+ if (mSafeToCloseClient) {
+ mHttpClient.close();
+ mHttpClient = null;
+ }
+ }
+ }
+
+ @Override
+ public void onPrepared(MediaPlayer player) {
+ if (!isCurrentPlayer(player)) {
+ return;
+ }
+ setState(STATE_READY);
+ if (DEBUG) {
+ Log.d(TAG, mDebugId + ": Finished preparing, seekOnReady is " + mSeekOnReady);
+ }
+ if (mSeekOnReady >= 0) {
+ onSeekTo(mSeekOnReady);
+ mSeekOnReady = -1;
+ }
+ if (mPlayOnReady) {
+ player.start();
+ setState(STATE_PLAYING);
+ }
+ }
+
+ @Override
+ public void onBufferingUpdate(MediaPlayer player, int percent) {
+ if (!isCurrentPlayer(player)) {
+ return;
+ }
+ pushOnBufferingUpdate(percent);
+ }
+
+ @Override
+ public void onCompletion(MediaPlayer player) {
+ if (!isCurrentPlayer(player)) {
+ return;
+ }
+ if (DEBUG) {
+ Log.d(TAG, mDebugId + ": Completed item. Have next item? " + (mNextPlayer != null));
+ }
+ if (mNextPlayer != null) {
+ if (mPlayer != null) {
+ mPlayer.release();
+ }
+ mPlayer = mNextPlayer;
+ mContent = mNextContent;
+ mNextPlayer = null;
+ mNextContent = null;
+ pushOnNextStarted();
+ return;
+ }
+ setState(STATE_ENDED);
+ }
+
+ @Override
+ public boolean onError(MediaPlayer player, int what, int extra) {
+ if (!isCurrentPlayer(player)) {
+ return false;
+ }
+ if (DEBUG) {
+ Log.d(TAG, mDebugId + ": Entered error state, what: " + what + " extra: " + extra);
+ }
+ synchronized (mErrorLock) {
+ ++mErrorId;
+ mError = new PlaybackError();
+ mError.type = what;
+ mError.extra = extra;
+ }
+
+ if (what == MediaPlayer.MEDIA_ERROR_UNKNOWN && extra == MediaPlayer.MEDIA_ERROR_IO
+ && mContent != null && mContent.source.startsWith("http")) {
+ HttpGet request = new HttpGet(mContent.source);
+ if (mContent.headers != null) {
+ for (String key : mContent.headers.keySet()) {
+ request.addHeader(key, mContent.headers.get(key));
+ }
+ }
+ synchronized (mErrorLock) {
+ if (mErrorRetriever != null) {
+ mErrorRetriever.cancelRequestLocked(false);
+ }
+ mErrorRetriever = new AsyncErrorRetriever(mErrorId);
+ mErrorRetriever.execute(request);
+ }
+ } else {
+ setError(what, extra, null, null);
+ }
+ return true;
+ }
+
+ @Override
+ public void onAudioFocusChange(int focusChange) {
+ // TODO figure out appropriate logic for handling focus loss at the TUQ
+ // level.
+ switch (focusChange) {
+ case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
+ case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
+ if (mState == STATE_PLAYING) {
+ onPause();
+ mPlayOnReady = true;
+ }
+ mHasAudioFocus = false;
+ break;
+ case AudioManager.AUDIOFOCUS_LOSS:
+ if (mState == STATE_PLAYING) {
+ onPause();
+ mPlayOnReady = false;
+ }
+ pushOnFocusLost();
+ mHasAudioFocus = false;
+ break;
+ case AudioManager.AUDIOFOCUS_GAIN:
+ mHasAudioFocus = true;
+ if (mPlayOnReady) {
+ onPlay();
+ }
+ break;
+ default:
+ Log.d(TAG, "Unknown focus change event " + focusChange);
+ break;
+ }
+ }
+
+ @Override
+ public void setContent(Bundle request) {
+ setContent(request, null);
+ }
+
+ /**
+ * Prepares the player for the given playback request. If the holder is null
+ * it is assumed this is an audio only source. If playOnReady is set to true
+ * the media will begin playing as soon as it can.
+ */
+ public void setContent(Bundle request, SurfaceHolder holder) {
+ String source = request.getString(RequestUtils.EXTRA_KEY_SOURCE);
+ Map<String, String> headers = null; // request.mHeaders;
+ boolean playOnReady = true; // request.mPlayOnReady;
+ if (DEBUG) {
+ Log.d(TAG, mDebugId + ": Settings new content. Have a player? " + (mPlayer != null)
+ + " have a next player? " + (mNextPlayer != null));
+ }
+ cleanUpPlayer();
+ setState(STATE_PREPARING);
+ mPlayOnReady = playOnReady;
+ mSeekOnReady = -1;
+ final MediaPlayer newPlayer = new MediaPlayer();
+
+ requestAudioFocus();
+
+ mPlayer = newPlayer;
+ mContent = new PlayerContent(source, headers);
+ try {
+ if (headers != null) {
+ Uri sourceUri = Uri.parse(source);
+ newPlayer.setDataSource(mContext, sourceUri, headers);
+ } else {
+ newPlayer.setDataSource(source);
+ }
+ } catch (Exception e) {
+ setError(Listener.ERROR_LOAD_FAILED, 0, null, e);
+ return;
+ }
+ if (isHolderReady(holder, newPlayer)) {
+ preparePlayer(newPlayer, true);
+ }
+ }
+
+ @Override
+ public void setNextContent(Bundle request) {
+ String source = request.getString(RequestUtils.EXTRA_KEY_SOURCE);
+ Map<String, String> headers = null; // request.mHeaders;
+
+ // TODO support video
+
+ if (DEBUG) {
+ Log.d(TAG, mDebugId + ": Setting next content. Have player? " + (mPlayer != null)
+ + " have next player? " + (mNextPlayer != null));
+ }
+
+ if (mPlayer == null) {
+ // The manager isn't being used to play anything, don't try to
+ // set a next.
+ return;
+ }
+ if (mNextPlayer != null) {
+ // Before setting up the new one clear out the old one and release
+ // it to ensure it doesn't play.
+ mPlayer.setNextMediaPlayer(null);
+ mNextPlayer.release();
+ mNextPlayer = null;
+ mNextContent = null;
+ }
+ if (source == null) {
+ // If there's no new content we're done
+ return;
+ }
+ final MediaPlayer newPlayer = new MediaPlayer();
+
+ try {
+ if (headers != null) {
+ Uri sourceUri = Uri.parse(source);
+ newPlayer.setDataSource(mContext, sourceUri, headers);
+ } else {
+ newPlayer.setDataSource(source);
+ }
+ } catch (Exception e) {
+ newPlayer.release();
+ // Don't return an error until we get to this item in playback
+ return;
+ }
+
+ if (preparePlayer(newPlayer, false)) {
+ mPlayer.setNextMediaPlayer(newPlayer);
+ mNextPlayer = newPlayer;
+ mNextContent = new PlayerContent(source, headers);
+ }
+ }
+
+ private void requestAudioFocus() {
+ int result = mAudioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC,
+ AudioManager.AUDIOFOCUS_GAIN);
+ mHasAudioFocus = result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
+ }
+
+ /**
+ * Start the player if possible or queue it to play when ready. If the
+ * player is in a state where it will never be ready returns false.
+ *
+ * @return true if the content was started or will be started later
+ */
+ @Override
+ public boolean onPlay() {
+ MediaPlayer player = mPlayer;
+ if (player != null && mState == STATE_PLAYING) {
+ // already playing, just return
+ return true;
+ }
+ if (!mHasAudioFocus) {
+ requestAudioFocus();
+ }
+ if (player != null && canPlay()) {
+ player.start();
+ setState(STATE_PLAYING);
+ } else if (canReadyPlay()) {
+ mPlayOnReady = true;
+ } else if (!isPlaying()) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Pause the player if possible or set it to not play when ready. If the
+ * player is in a state where it will never be ready returns false.
+ *
+ * @return true if the content was paused or will wait to play when ready
+ * later
+ */
+ @Override
+ public boolean onPause() {
+ MediaPlayer player = mPlayer;
+ if (player != null && (mState & CAN_PAUSE) != 0) {
+ player.pause();
+ setState(STATE_PAUSED);
+ } else if ((mState & CAN_READY_PLAY) != 0) {
+ mPlayOnReady = false;
+ } else if (!isPaused()) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Seek to a given position in the media. If the seek succeeded or will be
+ * performed when loading is complete returns true. If the position is not
+ * in range or the player will never be ready returns false.
+ *
+ * @param position The position to seek to in milliseconds
+ * @return true if playback was moved or will be moved when ready
+ */
+ @Override
+ public boolean onSeekTo(int position) {
+ MediaPlayer player = mPlayer;
+ if (player != null && (mState & CAN_SEEK) != 0) {
+ if (position < 0 || position >= getDuration()) {
+ return false;
+ } else {
+ if (mState == STATE_ENDED) {
+ player.start();
+ player.pause();
+ setState(STATE_PAUSED);
+ }
+ player.seekTo(position);
+ }
+ } else if ((mState & CAN_READY_SEEK) != 0) {
+ mSeekOnReady = position;
+ } else {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Stop the player. It cannot be used again until
+ * {@link #setContent(String, boolean)} is called.
+ *
+ * @return true if stopping the player succeeded
+ */
+ @Override
+ public boolean onStop() {
+ cleanUpPlayer();
+ setState(STATE_STOPPED);
+ return true;
+ }
+
+ public boolean isPlaying() {
+ return mState == STATE_PLAYING;
+ }
+
+ public boolean isPaused() {
+ return mState == STATE_PAUSED;
+ }
+
+ @Override
+ public long getSeekPosition() {
+ return ((mState & CAN_GET_POSITION) == 0) ? -1 : mPlayer.getCurrentPosition();
+ }
+
+ @Override
+ public long getDuration() {
+ return ((mState & CAN_GET_POSITION) == 0) ? -1 : mPlayer.getDuration();
+ }
+
+ private boolean canPlay() {
+ return ((mState & CAN_PLAY) != 0) && mHasAudioFocus;
+ }
+
+ private boolean canReadyPlay() {
+ return (mState & CAN_PLAY) != 0 || (mState & CAN_READY_PLAY) != 0;
+ }
+
+ /**
+ * Sends a state update if the listener exists
+ */
+ private void setState(int state) {
+ if (state == mState) {
+ return;
+ }
+ Log.d(TAG, "Entering state " + state + " from state " + mState);
+ mState = state;
+ if (state != STATE_ERROR) {
+ // Don't notify error here, it'll get sent via onError
+ pushOnStateChanged(state);
+ }
+ }
+
+ private boolean preparePlayer(final MediaPlayer player, boolean current) {
+ player.setOnPreparedListener(this);
+ player.setOnBufferingUpdateListener(this);
+ player.setOnCompletionListener(this);
+ player.setOnErrorListener(this);
+ try {
+ player.prepareAsync();
+ if (current) {
+ setState(STATE_PREPARING);
+ }
+ } catch (IllegalStateException e) {
+ if (current) {
+ setError(Listener.ERROR_PREPARE_ERROR, 0, null, e);
+ }
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * @param extra
+ * @param e
+ */
+ private void setError(int type, int extra, Bundle extras, Exception e) {
+ setState(STATE_ERROR);
+ pushOnError(type, extra, extras, e);
+ cleanUpPlayer();
+ return;
+ }
+
+ /**
+ * Checks if the holder is ready and either sets up a callback to wait for
+ * it or sets it directly. If
+ *
+ * @param holder
+ * @param player
+ * @return
+ */
+ private boolean isHolderReady(final SurfaceHolder holder, final MediaPlayer player) {
+ mHolder = holder;
+ if (holder != null) {
+ if (holder.getSurface() != null && holder.getSurface().isValid()) {
+ player.setDisplay(holder);
+ return true;
+ } else {
+ Log.w(TAG, "Holder not null, waiting for it to be ready");
+ // If the holder isn't ready yet add a callback to set the
+ // holder when it's ready.
+ SurfaceHolder.Callback cb = new SurfaceHolder.Callback() {
+ @Override
+ public void surfaceDestroyed(SurfaceHolder arg0) {
+ }
+
+ @Override
+ public void surfaceCreated(SurfaceHolder arg0) {
+ if (player.equals(mPlayer)) {
+ player.setDisplay(arg0);
+ preparePlayer(player, true);
+ }
+ }
+
+ @Override
+ public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2, int arg3) {
+ }
+ };
+ mHolderCB = cb;
+ holder.addCallback(cb);
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private void cleanUpPlayer() {
+ if (DEBUG) {
+ Log.d(TAG, mDebugId + ": Cleaning up current player");
+ }
+ synchronized (mErrorLock) {
+ mError = null;
+ if (mErrorRetriever != null) {
+ mErrorRetriever.cancelRequestLocked(false);
+ // Don't set to null as we may need to cancel again with true if
+ // the object gets destroyed.
+ }
+ }
+ mAudioManager.abandonAudioFocus(this);
+
+ SurfaceHolder.Callback cb = mHolderCB;
+ mHolderCB = null;
+ SurfaceHolder holder = mHolder;
+ mHolder = null;
+ if (holder != null && cb != null) {
+ holder.removeCallback(cb);
+ }
+
+ MediaPlayer player = mPlayer;
+ mPlayer = null;
+ if (player != null) {
+ player.reset();
+ player.release();
+ }
+ }
+
+ private boolean isCurrentPlayer(MediaPlayer player) {
+ return player.equals(mPlayer);
+ }
+}
diff --git a/tests/OneMedia/src/com/android/onemedia/playback/MediaItem.java b/tests/OneMedia/src/com/android/onemedia/playback/MediaItem.java
new file mode 100644
index 0000000..f9e6794
--- /dev/null
+++ b/tests/OneMedia/src/com/android/onemedia/playback/MediaItem.java
@@ -0,0 +1,59 @@
+package com.android.onemedia.playback;
+
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.support.v7.media.MediaItemMetadata;
+
+/**
+ * TODO: Insert description here. (generated by epastern)
+ */
+public class MediaItem implements Parcelable {
+ private Bundle mBundle;
+
+ public MediaItem() {
+
+ }
+
+ private MediaItem(Parcel in) {
+ mBundle = in.readBundle();
+ }
+
+ public String getTitle() {
+ return mBundle.getString(MediaItemMetadata.KEY_TITLE);
+ }
+
+ public String getArtist() {
+ return mBundle.getString(MediaItemMetadata.KEY_ALBUM_ARTIST);
+ }
+
+ /* (non-Javadoc)
+ * @see android.os.Parcelable#describeContents()
+ */
+ @Override
+ public int describeContents() {
+ // TODO(epastern): Auto-generated method stub
+ return 0;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see android.os.Parcelable#writeToParcel(android.os.Parcel, int)
+ */
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeBundle(mBundle);
+ }
+
+ public static final Parcelable.Creator<MediaItem> CREATOR
+ = new Parcelable.Creator<MediaItem>() {
+ public MediaItem createFromParcel(Parcel in) {
+ return new MediaItem(in);
+ }
+
+ public MediaItem[] newArray(int size) {
+ return new MediaItem[size];
+ }
+ };
+
+}
diff --git a/tests/OneMedia/src/com/android/onemedia/playback/PlaybackError.java b/tests/OneMedia/src/com/android/onemedia/playback/PlaybackError.java
new file mode 100644
index 0000000..72d936c
--- /dev/null
+++ b/tests/OneMedia/src/com/android/onemedia/playback/PlaybackError.java
@@ -0,0 +1,10 @@
+package com.android.onemedia.playback;
+
+import android.os.Bundle;
+
+public class PlaybackError {
+ public int type;
+ public int extra;
+ public String errorMessage;
+ public Bundle errorExtras;
+}
diff --git a/tests/OneMedia/src/com/android/onemedia/playback/Renderer.java b/tests/OneMedia/src/com/android/onemedia/playback/Renderer.java
new file mode 100644
index 0000000..2451bdf
--- /dev/null
+++ b/tests/OneMedia/src/com/android/onemedia/playback/Renderer.java
@@ -0,0 +1,199 @@
+package com.android.onemedia.playback;
+
+import android.content.Context;
+import android.media.MediaPlayer;
+import android.os.Bundle;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * TODO: Insert description here. (generated by epastern)
+ */
+public abstract class Renderer {
+ public static final String FEATURE_SET_CONTENT = "com.android.media.SET_CONTENT";
+ public static final String FEATURE_SET_NEXT_CONTENT = "com.android.media.SET_NEXT_CONTENT";
+ public static final String FEATURE_PLAY = "com.android.media.PLAY";
+ public static final String FEATURE_PAUSE = "com.android.media.PAUSE";
+ public static final String FEATURE_NEXT = "com.android.media.NEXT";
+ public static final String FEATURE_PREVIOUS = "com.android.media.PREVIOUS";
+ public static final String FEATURE_SEEK_TO = "com.android.media.SEEK_TO";
+ public static final String FEATURE_STOP = "com.android.media.STOP";
+ // TODO move states somewhere else
+ public static final int STATE_ERROR = 0;
+ /**
+ * The state MediaPlayerManager starts in before any action has been
+ * performed.
+ */
+ public static final int STATE_INIT = 1 << 0;
+ /**
+ * Indicates the source has been set and it is being prepared/buffered
+ * before starting playback.
+ */
+ public static final int STATE_PREPARING = 1 << 1;
+ /**
+ * The media is ready and playback can be started.
+ */
+ public static final int STATE_READY = 1 << 2;
+ /**
+ * The media is currently playing.
+ */
+ public static final int STATE_PLAYING = 1 << 3;
+ /**
+ * The media is currently paused.
+ */
+ public static final int STATE_PAUSED = 1 << 4;
+ /**
+ * The service has been stopped and cannot be started again until a new
+ * source has been set.
+ */
+ public static final int STATE_STOPPED = 1 << 5;
+ /**
+ * The playback has reached the end. It can be restarted by calling play().
+ */
+ public static final int STATE_ENDED = 1 << 6;
+
+ // TODO decide on proper way of describing features
+ protected List<String> mFeatures = new ArrayList<String>();
+ protected List<Listener> mListeners = new ArrayList<Listener>();
+
+ public Renderer(Context context, Bundle params) {
+ onCreate(params);
+ initFeatures(params);
+ }
+
+ abstract public void setContent(Bundle request);
+
+ public void onCreate(Bundle params) {
+ // Do nothing by default
+ }
+
+ public void setNextContent(Bundle request) {
+ throw new UnsupportedOperationException("setNextContent() is not supported.");
+ }
+
+ public List<String> getFeatures() {
+ return mFeatures;
+ }
+
+ public boolean onPlay() {
+ throw new UnsupportedOperationException("play is not supported.");
+ }
+
+ public boolean onPause() {
+ throw new UnsupportedOperationException("pause is not supported.");
+ }
+
+ public boolean onNext() {
+ throw new UnsupportedOperationException("next is not supported.");
+ }
+
+ public boolean onPrevious() {
+ throw new UnsupportedOperationException("previous is not supported.");
+ }
+
+ public boolean onStop() {
+ throw new UnsupportedOperationException("stop is not supported.");
+ }
+
+ public boolean onSeekTo(int time) {
+ throw new UnsupportedOperationException("seekTo is not supported.");
+ }
+
+ public long getSeekPosition() {
+ throw new UnsupportedOperationException("getSeekPosition is not supported.");
+ }
+
+ public long getDuration() {
+ throw new UnsupportedOperationException("getDuration is not supported.");
+ }
+
+ public int getPlayState() {
+ throw new UnsupportedOperationException("getPlayState is not supported.");
+ }
+
+ public void onDestroy() {
+ // Do nothing by default
+ }
+
+ public void registerListener(Listener listener) {
+ if (!mListeners.contains(listener)) {
+ mListeners.add(listener);
+ }
+ }
+
+ public void unregisterListener(Listener listener) {
+ mListeners.remove(listener);
+ }
+
+ protected void initFeatures(Bundle params) {
+ mFeatures.add(FEATURE_SET_CONTENT);
+ }
+
+ protected void pushOnError(int type, int extra, Bundle extras, Throwable error) {
+ for (Listener listener : mListeners) {
+ listener.onError(type, extra, extras, error);
+ }
+ }
+
+ protected void pushOnStateChanged(int newState) {
+ for (Listener listener : mListeners) {
+ listener.onStateChanged(newState);
+ }
+ }
+
+ protected void pushOnBufferingUpdate(int percent) {
+ for (Listener listener : mListeners) {
+ listener.onBufferingUpdate(percent);
+ }
+ }
+
+ protected void pushOnFocusLost() {
+ for (Listener listener : mListeners) {
+ listener.onFocusLost();
+ }
+ }
+
+ protected void pushOnNextStarted() {
+ for (Listener listener : mListeners) {
+ listener.onNextStarted();
+ }
+ }
+
+ public interface Listener {
+ public static final int ERROR_LOAD_FAILED = 1770;
+ public static final int ERROR_PREPARE_ERROR = 1771;
+ public static final int ERROR_PLAYBACK_FAILED = 1772;
+
+ /**
+ * When an error occurs onError will be called but not onStateChanged.
+ * The Manager will remain in the error state until
+ * {@link #setContent()} is called again.
+ */
+ public void onError(int type, int extra, Bundle extras,
+ Throwable error);
+
+ /**
+ * onStateChanged will be called whenever the state of the manager
+ * transitions except to an error state.
+ */
+ public void onStateChanged(int newState);
+
+ /**
+ * This is a passthrough of
+ * {@link MediaPlayer.OnBufferingUpdateListener}.
+ */
+ public void onBufferingUpdate(int percent);
+
+ /**
+ * Called when audio focus is lost and it is not transient or ducking.
+ */
+ public void onFocusLost();
+
+ /**
+ * Called when the next item was started playing. Only called if a next
+ * item has been set and the current item has ended.
+ */
+ public void onNextStarted();
+ }
+}
diff --git a/tests/OneMedia/src/com/android/onemedia/playback/RendererFactory.java b/tests/OneMedia/src/com/android/onemedia/playback/RendererFactory.java
new file mode 100644
index 0000000..f333fce
--- /dev/null
+++ b/tests/OneMedia/src/com/android/onemedia/playback/RendererFactory.java
@@ -0,0 +1,22 @@
+package com.android.onemedia.playback;
+
+import android.content.Context;
+import android.media.MediaRouter;
+import android.os.Bundle;
+import android.util.Log;
+
+/**
+ * TODO: Insert description here.
+ */
+public class RendererFactory {
+ private static final String TAG = "RendererFactory";
+
+ public Renderer createRenderer(MediaRouter.RouteInfo route, Context context, Bundle params) {
+ if (route.getPlaybackType() == MediaRouter.RouteInfo.PLAYBACK_TYPE_LOCAL) {
+ return new LocalRenderer(context, params);
+ }
+ Log.e(TAG, "Unable to create renderer for route of playback type "
+ + route.getPlaybackType());
+ return null;
+ }
+}
diff --git a/tests/OneMedia/src/com/android/onemedia/playback/RequestUtils.java b/tests/OneMedia/src/com/android/onemedia/playback/RequestUtils.java
new file mode 100644
index 0000000..9b50dad
--- /dev/null
+++ b/tests/OneMedia/src/com/android/onemedia/playback/RequestUtils.java
@@ -0,0 +1,53 @@
+package com.android.onemedia.playback;
+
+import android.os.Bundle;
+import android.support.v7.media.MediaItemMetadata;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * TODO: Insert description here. (generated by epastern)
+ */
+public class RequestUtils {
+ public static final String ACTION_SET_CONTENT = "set_content";
+ public static final String ACTION_SET_NEXT_CONTENT = "set_next_content";
+
+ public static final String EXTRA_KEY_SOURCE = "source";
+ public static final String EXTRA_KEY_METADATA = "metadata";
+ public static final String EXTRA_KEY_HEADERS = "headers";
+
+ private RequestUtils() {
+ }
+
+ public static class ContentBuilder {
+ private Bundle mBundle;
+
+ public ContentBuilder() {
+ mBundle = new Bundle();
+ }
+
+ public ContentBuilder setSource(String source) {
+ mBundle.putString(EXTRA_KEY_SOURCE, source);
+ return this;
+ }
+
+ /**
+ * @see MediaItemMetadata
+ * @param metadata The metadata for this item
+ */
+ public ContentBuilder setMetadata(Bundle metadata) {
+ mBundle.putBundle(EXTRA_KEY_METADATA, metadata);
+ return this;
+ }
+
+ public ContentBuilder setHeaders(HashMap<String, String> headers) {
+ mBundle.putSerializable(EXTRA_KEY_HEADERS, headers);
+ return this;
+ }
+
+ public Bundle build() {
+ return mBundle;
+ }
+ }
+}