diff options
author | RoboErik <epastern@google.com> | 2014-02-13 14:22:42 -0800 |
---|---|---|
committer | RoboErik <epastern@google.com> | 2014-02-19 13:41:38 -0800 |
commit | bfa153b64b4e8c2faa39a15e87fc9f0300335f20 (patch) | |
tree | 2170cb41d7a69560a0a2390528ea1319c2916e1d /tests | |
parent | 01fe661ae5da3739215d93922412df4b24c859a2 (diff) | |
download | frameworks_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')
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 Binary files differnew file mode 100644 index 0000000..288b665 --- /dev/null +++ b/tests/OneMedia/res/drawable-hdpi/ic_launcher.png diff --git a/tests/OneMedia/res/drawable-mdpi/ic_launcher.png b/tests/OneMedia/res/drawable-mdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000..6ae570b --- /dev/null +++ b/tests/OneMedia/res/drawable-mdpi/ic_launcher.png diff --git a/tests/OneMedia/res/drawable-xhdpi/ic_launcher.png b/tests/OneMedia/res/drawable-xhdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000..d4fb7cd --- /dev/null +++ b/tests/OneMedia/res/drawable-xhdpi/ic_launcher.png diff --git a/tests/OneMedia/res/drawable-xxhdpi/ic_launcher.png b/tests/OneMedia/res/drawable-xxhdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000..85a6081 --- /dev/null +++ b/tests/OneMedia/res/drawable-xxhdpi/ic_launcher.png 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; + } + } +} |