diff options
Diffstat (limited to 'samples/browseable/MediaRouter/src')
16 files changed, 4276 insertions, 0 deletions
diff --git a/samples/browseable/MediaRouter/src/com.example.android.common.logger/Log.java b/samples/browseable/MediaRouter/src/com.example.android.common.logger/Log.java new file mode 100644 index 000000000..17503c568 --- /dev/null +++ b/samples/browseable/MediaRouter/src/com.example.android.common.logger/Log.java @@ -0,0 +1,236 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.common.logger; + +/** + * Helper class for a list (or tree) of LoggerNodes. + * + * <p>When this is set as the head of the list, + * an instance of it can function as a drop-in replacement for {@link android.util.Log}. + * Most of the methods in this class server only to map a method call in Log to its equivalent + * in LogNode.</p> + */ +public class Log { + // Grabbing the native values from Android's native logging facilities, + // to make for easy migration and interop. + public static final int NONE = -1; + public static final int VERBOSE = android.util.Log.VERBOSE; + public static final int DEBUG = android.util.Log.DEBUG; + public static final int INFO = android.util.Log.INFO; + public static final int WARN = android.util.Log.WARN; + public static final int ERROR = android.util.Log.ERROR; + public static final int ASSERT = android.util.Log.ASSERT; + + // Stores the beginning of the LogNode topology. + private static LogNode mLogNode; + + /** + * Returns the next LogNode in the linked list. + */ + public static LogNode getLogNode() { + return mLogNode; + } + + /** + * Sets the LogNode data will be sent to. + */ + public static void setLogNode(LogNode node) { + mLogNode = node; + } + + /** + * Instructs the LogNode to print the log data provided. Other LogNodes can + * be chained to the end of the LogNode as desired. + * + * @param priority Log level of the data being logged. Verbose, Error, etc. + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void println(int priority, String tag, String msg, Throwable tr) { + if (mLogNode != null) { + mLogNode.println(priority, tag, msg, tr); + } + } + + /** + * Instructs the LogNode to print the log data provided. Other LogNodes can + * be chained to the end of the LogNode as desired. + * + * @param priority Log level of the data being logged. Verbose, Error, etc. + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. The actual message to be logged. + */ + public static void println(int priority, String tag, String msg) { + println(priority, tag, msg, null); + } + + /** + * Prints a message at VERBOSE priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void v(String tag, String msg, Throwable tr) { + println(VERBOSE, tag, msg, tr); + } + + /** + * Prints a message at VERBOSE priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + */ + public static void v(String tag, String msg) { + v(tag, msg, null); + } + + + /** + * Prints a message at DEBUG priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void d(String tag, String msg, Throwable tr) { + println(DEBUG, tag, msg, tr); + } + + /** + * Prints a message at DEBUG priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + */ + public static void d(String tag, String msg) { + d(tag, msg, null); + } + + /** + * Prints a message at INFO priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void i(String tag, String msg, Throwable tr) { + println(INFO, tag, msg, tr); + } + + /** + * Prints a message at INFO priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + */ + public static void i(String tag, String msg) { + i(tag, msg, null); + } + + /** + * Prints a message at WARN priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void w(String tag, String msg, Throwable tr) { + println(WARN, tag, msg, tr); + } + + /** + * Prints a message at WARN priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + */ + public static void w(String tag, String msg) { + w(tag, msg, null); + } + + /** + * Prints a message at WARN priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void w(String tag, Throwable tr) { + w(tag, null, tr); + } + + /** + * Prints a message at ERROR priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void e(String tag, String msg, Throwable tr) { + println(ERROR, tag, msg, tr); + } + + /** + * Prints a message at ERROR priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + */ + public static void e(String tag, String msg) { + e(tag, msg, null); + } + + /** + * Prints a message at ASSERT priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void wtf(String tag, String msg, Throwable tr) { + println(ASSERT, tag, msg, tr); + } + + /** + * Prints a message at ASSERT priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + */ + public static void wtf(String tag, String msg) { + wtf(tag, msg, null); + } + + /** + * Prints a message at ASSERT priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void wtf(String tag, Throwable tr) { + wtf(tag, null, tr); + } +} diff --git a/samples/browseable/MediaRouter/src/com.example.android.common.logger/LogFragment.java b/samples/browseable/MediaRouter/src/com.example.android.common.logger/LogFragment.java new file mode 100644 index 000000000..b302acd4b --- /dev/null +++ b/samples/browseable/MediaRouter/src/com.example.android.common.logger/LogFragment.java @@ -0,0 +1,109 @@ +/* +* Copyright 2013 The Android Open Source Project +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +/* + * Copyright 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.common.logger; + +import android.graphics.Typeface; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ScrollView; + +/** + * Simple fraggment which contains a LogView and uses is to output log data it receives + * through the LogNode interface. + */ +public class LogFragment extends Fragment { + + private LogView mLogView; + private ScrollView mScrollView; + + public LogFragment() {} + + public View inflateViews() { + mScrollView = new ScrollView(getActivity()); + ViewGroup.LayoutParams scrollParams = new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT); + mScrollView.setLayoutParams(scrollParams); + + mLogView = new LogView(getActivity()); + ViewGroup.LayoutParams logParams = new ViewGroup.LayoutParams(scrollParams); + logParams.height = ViewGroup.LayoutParams.WRAP_CONTENT; + mLogView.setLayoutParams(logParams); + mLogView.setClickable(true); + mLogView.setFocusable(true); + mLogView.setTypeface(Typeface.MONOSPACE); + + // Want to set padding as 16 dips, setPadding takes pixels. Hooray math! + int paddingDips = 16; + double scale = getResources().getDisplayMetrics().density; + int paddingPixels = (int) ((paddingDips * (scale)) + .5); + mLogView.setPadding(paddingPixels, paddingPixels, paddingPixels, paddingPixels); + mLogView.setCompoundDrawablePadding(paddingPixels); + + mLogView.setGravity(Gravity.BOTTOM); + mLogView.setTextAppearance(getActivity(), android.R.style.TextAppearance_Holo_Medium); + + mScrollView.addView(mLogView); + return mScrollView; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + + View result = inflateViews(); + + mLogView.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) {} + + @Override + public void afterTextChanged(Editable s) { + mScrollView.fullScroll(ScrollView.FOCUS_DOWN); + } + }); + return result; + } + + public LogView getLogView() { + return mLogView; + } +}
\ No newline at end of file diff --git a/samples/browseable/MediaRouter/src/com.example.android.common.logger/LogNode.java b/samples/browseable/MediaRouter/src/com.example.android.common.logger/LogNode.java new file mode 100644 index 000000000..bc37cabc0 --- /dev/null +++ b/samples/browseable/MediaRouter/src/com.example.android.common.logger/LogNode.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.common.logger; + +/** + * Basic interface for a logging system that can output to one or more targets. + * Note that in addition to classes that will output these logs in some format, + * one can also implement this interface over a filter and insert that in the chain, + * such that no targets further down see certain data, or see manipulated forms of the data. + * You could, for instance, write a "ToHtmlLoggerNode" that just converted all the log data + * it received to HTML and sent it along to the next node in the chain, without printing it + * anywhere. + */ +public interface LogNode { + + /** + * Instructs first LogNode in the list to print the log data provided. + * @param priority Log level of the data being logged. Verbose, Error, etc. + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public void println(int priority, String tag, String msg, Throwable tr); + +} diff --git a/samples/browseable/MediaRouter/src/com.example.android.common.logger/LogView.java b/samples/browseable/MediaRouter/src/com.example.android.common.logger/LogView.java new file mode 100644 index 000000000..c01542b91 --- /dev/null +++ b/samples/browseable/MediaRouter/src/com.example.android.common.logger/LogView.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.common.logger; + +import android.app.Activity; +import android.content.Context; +import android.util.*; +import android.widget.TextView; + +/** Simple TextView which is used to output log data received through the LogNode interface. +*/ +public class LogView extends TextView implements LogNode { + + public LogView(Context context) { + super(context); + } + + public LogView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public LogView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + /** + * Formats the log data and prints it out to the LogView. + * @param priority Log level of the data being logged. Verbose, Error, etc. + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + @Override + public void println(int priority, String tag, String msg, Throwable tr) { + + + String priorityStr = null; + + // For the purposes of this View, we want to print the priority as readable text. + switch(priority) { + case android.util.Log.VERBOSE: + priorityStr = "VERBOSE"; + break; + case android.util.Log.DEBUG: + priorityStr = "DEBUG"; + break; + case android.util.Log.INFO: + priorityStr = "INFO"; + break; + case android.util.Log.WARN: + priorityStr = "WARN"; + break; + case android.util.Log.ERROR: + priorityStr = "ERROR"; + break; + case android.util.Log.ASSERT: + priorityStr = "ASSERT"; + break; + default: + break; + } + + // Handily, the Log class has a facility for converting a stack trace into a usable string. + String exceptionStr = null; + if (tr != null) { + exceptionStr = android.util.Log.getStackTraceString(tr); + } + + // Take the priority, tag, message, and exception, and concatenate as necessary + // into one usable line of text. + final StringBuilder outputBuilder = new StringBuilder(); + + String delimiter = "\t"; + appendIfNotNull(outputBuilder, priorityStr, delimiter); + appendIfNotNull(outputBuilder, tag, delimiter); + appendIfNotNull(outputBuilder, msg, delimiter); + appendIfNotNull(outputBuilder, exceptionStr, delimiter); + + // In case this was originally called from an AsyncTask or some other off-UI thread, + // make sure the update occurs within the UI thread. + ((Activity) getContext()).runOnUiThread( (new Thread(new Runnable() { + @Override + public void run() { + // Display the text we just generated within the LogView. + appendToLog(outputBuilder.toString()); + } + }))); + + if (mNext != null) { + mNext.println(priority, tag, msg, tr); + } + } + + public LogNode getNext() { + return mNext; + } + + public void setNext(LogNode node) { + mNext = node; + } + + /** Takes a string and adds to it, with a separator, if the bit to be added isn't null. Since + * the logger takes so many arguments that might be null, this method helps cut out some of the + * agonizing tedium of writing the same 3 lines over and over. + * @param source StringBuilder containing the text to append to. + * @param addStr The String to append + * @param delimiter The String to separate the source and appended strings. A tab or comma, + * for instance. + * @return The fully concatenated String as a StringBuilder + */ + private StringBuilder appendIfNotNull(StringBuilder source, String addStr, String delimiter) { + if (addStr != null) { + if (addStr.length() == 0) { + delimiter = ""; + } + + return source.append(addStr).append(delimiter); + } + return source; + } + + // The next LogNode in the chain. + LogNode mNext; + + /** Outputs the string as a new line of log data in the LogView. */ + public void appendToLog(String s) { + append("\n" + s); + } + + +} diff --git a/samples/browseable/MediaRouter/src/com.example.android.common.logger/LogWrapper.java b/samples/browseable/MediaRouter/src/com.example.android.common.logger/LogWrapper.java new file mode 100644 index 000000000..16a9e7ba2 --- /dev/null +++ b/samples/browseable/MediaRouter/src/com.example.android.common.logger/LogWrapper.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.common.logger; + +import android.util.Log; + +/** + * Helper class which wraps Android's native Log utility in the Logger interface. This way + * normal DDMS output can be one of the many targets receiving and outputting logs simultaneously. + */ +public class LogWrapper implements LogNode { + + // For piping: The next node to receive Log data after this one has done its work. + private LogNode mNext; + + /** + * Returns the next LogNode in the linked list. + */ + public LogNode getNext() { + return mNext; + } + + /** + * Sets the LogNode data will be sent to.. + */ + public void setNext(LogNode node) { + mNext = node; + } + + /** + * Prints data out to the console using Android's native log mechanism. + * @param priority Log level of the data being logged. Verbose, Error, etc. + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + @Override + public void println(int priority, String tag, String msg, Throwable tr) { + // There actually are log methods that don't take a msg parameter. For now, + // if that's the case, just convert null to the empty string and move on. + String useMsg = msg; + if (useMsg == null) { + useMsg = ""; + } + + // If an exeption was provided, convert that exception to a usable string and attach + // it to the end of the msg method. + if (tr != null) { + msg += "\n" + Log.getStackTraceString(tr); + } + + // This is functionally identical to Log.x(tag, useMsg); + // For instance, if priority were Log.VERBOSE, this would be the same as Log.v(tag, useMsg) + Log.println(priority, tag, useMsg); + + // If this isn't the last node in the chain, move things along. + if (mNext != null) { + mNext.println(priority, tag, msg, tr); + } + } +} diff --git a/samples/browseable/MediaRouter/src/com.example.android.common.logger/MessageOnlyLogFilter.java b/samples/browseable/MediaRouter/src/com.example.android.common.logger/MessageOnlyLogFilter.java new file mode 100644 index 000000000..19967dcd4 --- /dev/null +++ b/samples/browseable/MediaRouter/src/com.example.android.common.logger/MessageOnlyLogFilter.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.common.logger; + +/** + * Simple {@link LogNode} filter, removes everything except the message. + * Useful for situations like on-screen log output where you don't want a lot of metadata displayed, + * just easy-to-read message updates as they're happening. + */ +public class MessageOnlyLogFilter implements LogNode { + + LogNode mNext; + + /** + * Takes the "next" LogNode as a parameter, to simplify chaining. + * + * @param next The next LogNode in the pipeline. + */ + public MessageOnlyLogFilter(LogNode next) { + mNext = next; + } + + public MessageOnlyLogFilter() { + } + + @Override + public void println(int priority, String tag, String msg, Throwable tr) { + if (mNext != null) { + getNext().println(Log.NONE, null, msg, null); + } + } + + /** + * Returns the next LogNode in the chain. + */ + public LogNode getNext() { + return mNext; + } + + /** + * Sets the LogNode data will be sent to.. + */ + public void setNext(LogNode node) { + mNext = node; + } + +} diff --git a/samples/browseable/MediaRouter/src/com.example.android.mediarouter/player/LocalPlayer.java b/samples/browseable/MediaRouter/src/com.example.android.mediarouter/player/LocalPlayer.java new file mode 100644 index 000000000..86f51001d --- /dev/null +++ b/samples/browseable/MediaRouter/src/com.example.android.mediarouter/player/LocalPlayer.java @@ -0,0 +1,630 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.mediarouter.player; + +import android.app.Activity; +import android.app.Presentation; +import android.content.Context; +import android.content.DialogInterface; +import android.media.MediaPlayer; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.SystemClock; +import android.support.v7.media.MediaItemStatus; +import android.support.v7.media.MediaRouter.RouteInfo; +import android.util.Log; +import android.view.Display; +import android.view.Gravity; +import android.view.Surface; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.FrameLayout; + +import com.example.android.mediarouter.R; + +import java.io.IOException; + +/** + * Handles playback of a single media item using MediaPlayer. + */ +public abstract class LocalPlayer extends Player implements + MediaPlayer.OnPreparedListener, + MediaPlayer.OnCompletionListener, + MediaPlayer.OnErrorListener, + MediaPlayer.OnSeekCompleteListener { + private static final String TAG = "LocalPlayer"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + private static final int STATE_IDLE = 0; + private static final int STATE_PLAY_PENDING = 1; + private static final int STATE_READY = 2; + private static final int STATE_PLAYING = 3; + private static final int STATE_PAUSED = 4; + + private final Context mContext; + private final Handler mHandler = new Handler(); + private MediaPlayer mMediaPlayer; + private int mState = STATE_IDLE; + private int mSeekToPos; + private int mVideoWidth; + private int mVideoHeight; + private Surface mSurface; + private SurfaceHolder mSurfaceHolder; + + public LocalPlayer(Context context) { + mContext = context; + + // reset media player + reset(); + } + + @Override + public boolean isRemotePlayback() { + return false; + } + + @Override + public boolean isQueuingSupported() { + return false; + } + + @Override + public void connect(RouteInfo route) { + if (DEBUG) { + Log.d(TAG, "connecting to: " + route); + } + } + + @Override + public void release() { + if (DEBUG) { + Log.d(TAG, "releasing"); + } + // release media player + if (mMediaPlayer != null) { + mMediaPlayer.stop(); + mMediaPlayer.release(); + mMediaPlayer = null; + } + } + + // Player + @Override + public void play(final PlaylistItem item) { + if (DEBUG) { + Log.d(TAG, "play: item=" + item); + } + reset(); + mSeekToPos = (int)item.getPosition(); + try { + mMediaPlayer.setDataSource(mContext, item.getUri()); + mMediaPlayer.prepareAsync(); + } catch (IllegalStateException e) { + Log.e(TAG, "MediaPlayer throws IllegalStateException, uri=" + item.getUri()); + } catch (IOException e) { + Log.e(TAG, "MediaPlayer throws IOException, uri=" + item.getUri()); + } catch (IllegalArgumentException e) { + Log.e(TAG, "MediaPlayer throws IllegalArgumentException, uri=" + item.getUri()); + } catch (SecurityException e) { + Log.e(TAG, "MediaPlayer throws SecurityException, uri=" + item.getUri()); + } + if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PLAYING) { + resume(); + } else { + pause(); + } + } + + @Override + public void seek(final PlaylistItem item) { + if (DEBUG) { + Log.d(TAG, "seek: item=" + item); + } + int pos = (int)item.getPosition(); + if (mState == STATE_PLAYING || mState == STATE_PAUSED) { + mMediaPlayer.seekTo(pos); + mSeekToPos = pos; + } else if (mState == STATE_IDLE || mState == STATE_PLAY_PENDING) { + // Seek before onPrepared() arrives, + // need to performed delayed seek in onPrepared() + mSeekToPos = pos; + } + } + + @Override + public void getStatus(final PlaylistItem item, final boolean update) { + if (mState == STATE_PLAYING || mState == STATE_PAUSED) { + // use mSeekToPos if we're currently seeking (mSeekToPos is reset + // when seeking is completed) + item.setDuration(mMediaPlayer.getDuration()); + item.setPosition(mSeekToPos > 0 ? + mSeekToPos : mMediaPlayer.getCurrentPosition()); + item.setTimestamp(SystemClock.elapsedRealtime()); + } + if (update && mCallback != null) { + mCallback.onPlaylistReady(); + } + } + + @Override + public void pause() { + if (DEBUG) { + Log.d(TAG, "pause"); + } + if (mState == STATE_PLAYING) { + mMediaPlayer.pause(); + mState = STATE_PAUSED; + } + } + + @Override + public void resume() { + if (DEBUG) { + Log.d(TAG, "resume"); + } + if (mState == STATE_READY || mState == STATE_PAUSED) { + mMediaPlayer.start(); + mState = STATE_PLAYING; + } else if (mState == STATE_IDLE){ + mState = STATE_PLAY_PENDING; + } + } + + @Override + public void stop() { + if (DEBUG) { + Log.d(TAG, "stop"); + } + if (mState == STATE_PLAYING || mState == STATE_PAUSED) { + mMediaPlayer.stop(); + mState = STATE_IDLE; + } + } + + @Override + public void enqueue(final PlaylistItem item) { + throw new UnsupportedOperationException("LocalPlayer doesn't support enqueue!"); + } + + @Override + public PlaylistItem remove(String iid) { + throw new UnsupportedOperationException("LocalPlayer doesn't support remove!"); + } + + //MediaPlayer Listeners + @Override + public void onPrepared(MediaPlayer mp) { + if (DEBUG) { + Log.d(TAG, "onPrepared"); + } + mHandler.post(new Runnable() { + @Override + public void run() { + if (mState == STATE_IDLE) { + mState = STATE_READY; + updateVideoRect(); + } else if (mState == STATE_PLAY_PENDING) { + mState = STATE_PLAYING; + updateVideoRect(); + if (mSeekToPos > 0) { + if (DEBUG) { + Log.d(TAG, "seek to initial pos: " + mSeekToPos); + } + mMediaPlayer.seekTo(mSeekToPos); + } + mMediaPlayer.start(); + } + if (mCallback != null) { + mCallback.onPlaylistChanged(); + } + } + }); + } + + @Override + public void onCompletion(MediaPlayer mp) { + if (DEBUG) { + Log.d(TAG, "onCompletion"); + } + mHandler.post(new Runnable() { + @Override + public void run() { + if (mCallback != null) { + mCallback.onCompletion(); + } + } + }); + } + + @Override + public boolean onError(MediaPlayer mp, int what, int extra) { + if (DEBUG) { + Log.d(TAG, "onError"); + } + mHandler.post(new Runnable() { + @Override + public void run() { + if (mCallback != null) { + mCallback.onError(); + } + } + }); + // return true so that onCompletion is not called + return true; + } + + @Override + public void onSeekComplete(MediaPlayer mp) { + if (DEBUG) { + Log.d(TAG, "onSeekComplete"); + } + mHandler.post(new Runnable() { + @Override + public void run() { + mSeekToPos = 0; + if (mCallback != null) { + mCallback.onPlaylistChanged(); + } + } + }); + } + + protected Context getContext() { return mContext; } + protected MediaPlayer getMediaPlayer() { return mMediaPlayer; } + protected int getVideoWidth() { return mVideoWidth; } + protected int getVideoHeight() { return mVideoHeight; } + protected void setSurface(Surface surface) { + mSurface = surface; + mSurfaceHolder = null; + updateSurface(); + } + + protected void setSurface(SurfaceHolder surfaceHolder) { + mSurface = null; + mSurfaceHolder = surfaceHolder; + updateSurface(); + } + + protected void removeSurface(SurfaceHolder surfaceHolder) { + if (surfaceHolder == mSurfaceHolder) { + setSurface((SurfaceHolder)null); + } + } + + protected void updateSurface() { + if (mMediaPlayer == null) { + // just return if media player is already gone + return; + } + if (mSurface != null) { + // The setSurface API does not exist until V14+. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + ICSMediaPlayer.setSurface(mMediaPlayer, mSurface); + } else { + throw new UnsupportedOperationException("MediaPlayer does not support " + + "setSurface() on this version of the platform."); + } + } else if (mSurfaceHolder != null) { + mMediaPlayer.setDisplay(mSurfaceHolder); + } else { + mMediaPlayer.setDisplay(null); + } + } + + protected abstract void updateSize(); + + private void reset() { + if (mMediaPlayer != null) { + mMediaPlayer.stop(); + mMediaPlayer.release(); + mMediaPlayer = null; + } + mMediaPlayer = new MediaPlayer(); + mMediaPlayer.setOnPreparedListener(this); + mMediaPlayer.setOnCompletionListener(this); + mMediaPlayer.setOnErrorListener(this); + mMediaPlayer.setOnSeekCompleteListener(this); + updateSurface(); + mState = STATE_IDLE; + mSeekToPos = 0; + } + + private void updateVideoRect() { + if (mState != STATE_IDLE && mState != STATE_PLAY_PENDING) { + int width = mMediaPlayer.getVideoWidth(); + int height = mMediaPlayer.getVideoHeight(); + if (width > 0 && height > 0) { + mVideoWidth = width; + mVideoHeight = height; + updateSize(); + } else { + Log.e(TAG, "video rect is 0x0!"); + mVideoWidth = mVideoHeight = 0; + } + } + } + + private static final class ICSMediaPlayer { + public static final void setSurface(MediaPlayer player, Surface surface) { + player.setSurface(surface); + } + } + + /** + * Handles playback of a single media item using MediaPlayer in SurfaceView + */ + public static class SurfaceViewPlayer extends LocalPlayer implements + SurfaceHolder.Callback { + private static final String TAG = "SurfaceViewPlayer"; + private RouteInfo mRoute; + private final SurfaceView mSurfaceView; + private final FrameLayout mLayout; + private DemoPresentation mPresentation; + + public SurfaceViewPlayer(Context context) { + super(context); + + mLayout = (FrameLayout)((Activity)context).findViewById(R.id.player); + mSurfaceView = (SurfaceView)((Activity)context).findViewById(R.id.surface_view); + + // add surface holder callback + SurfaceHolder holder = mSurfaceView.getHolder(); + holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); + holder.addCallback(this); + } + + @Override + public void connect(RouteInfo route) { + super.connect(route); + mRoute = route; + } + + @Override + public void release() { + super.release(); + + // dismiss presentation display + if (mPresentation != null) { + Log.i(TAG, "Dismissing presentation because the activity is no longer visible."); + mPresentation.dismiss(); + mPresentation = null; + } + + // remove surface holder callback + SurfaceHolder holder = mSurfaceView.getHolder(); + holder.removeCallback(this); + + // hide the surface view when SurfaceViewPlayer is destroyed + mSurfaceView.setVisibility(View.GONE); + mLayout.setVisibility(View.GONE); + } + + @Override + public void updatePresentation() { + // Get the current route and its presentation display. + Display presentationDisplay = mRoute != null ? mRoute.getPresentationDisplay() : null; + + // Dismiss the current presentation if the display has changed. + if (mPresentation != null && mPresentation.getDisplay() != presentationDisplay) { + Log.i(TAG, "Dismissing presentation because the current route no longer " + + "has a presentation display."); + mPresentation.dismiss(); + mPresentation = null; + } + + // Show a new presentation if needed. + if (mPresentation == null && presentationDisplay != null) { + Log.i(TAG, "Showing presentation on display: " + presentationDisplay); + mPresentation = new DemoPresentation(getContext(), presentationDisplay); + mPresentation.setOnDismissListener(mOnDismissListener); + try { + mPresentation.show(); + } catch (WindowManager.InvalidDisplayException ex) { + Log.w(TAG, "Couldn't show presentation! Display was removed in " + + "the meantime.", ex); + mPresentation = null; + } + } + + updateContents(); + } + + // SurfaceHolder.Callback + @Override + public void surfaceChanged(SurfaceHolder holder, int format, + int width, int height) { + if (DEBUG) { + Log.d(TAG, "surfaceChanged: " + width + "x" + height); + } + setSurface(holder); + } + + @Override + public void surfaceCreated(SurfaceHolder holder) { + if (DEBUG) { + Log.d(TAG, "surfaceCreated"); + } + setSurface(holder); + updateSize(); + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + if (DEBUG) { + Log.d(TAG, "surfaceDestroyed"); + } + removeSurface(holder); + } + + @Override + protected void updateSize() { + int width = getVideoWidth(); + int height = getVideoHeight(); + if (width > 0 && height > 0) { + if (mPresentation == null) { + int surfaceWidth = mLayout.getWidth(); + int surfaceHeight = mLayout.getHeight(); + + // Calculate the new size of mSurfaceView, so that video is centered + // inside the framelayout with proper letterboxing/pillarboxing + ViewGroup.LayoutParams lp = mSurfaceView.getLayoutParams(); + if (surfaceWidth * height < surfaceHeight * width) { + // Black bars on top&bottom, mSurfaceView has full layout width, + // while height is derived from video's aspect ratio + lp.width = surfaceWidth; + lp.height = surfaceWidth * height / width; + } else { + // Black bars on left&right, mSurfaceView has full layout height, + // while width is derived from video's aspect ratio + lp.width = surfaceHeight * width / height; + lp.height = surfaceHeight; + } + Log.i(TAG, "video rect is " + lp.width + "x" + lp.height); + mSurfaceView.setLayoutParams(lp); + } else { + mPresentation.updateSize(width, height); + } + } + } + + private void updateContents() { + // Show either the content in the main activity or the content in the presentation + if (mPresentation != null) { + mLayout.setVisibility(View.GONE); + mSurfaceView.setVisibility(View.GONE); + } else { + mLayout.setVisibility(View.VISIBLE); + mSurfaceView.setVisibility(View.VISIBLE); + } + } + + // Listens for when presentations are dismissed. + private final DialogInterface.OnDismissListener mOnDismissListener = + new DialogInterface.OnDismissListener() { + @Override + public void onDismiss(DialogInterface dialog) { + if (dialog == mPresentation) { + Log.i(TAG, "Presentation dismissed."); + mPresentation = null; + updateContents(); + } + } + }; + + // Presentation + private final class DemoPresentation extends Presentation { + private SurfaceView mPresentationSurfaceView; + + public DemoPresentation(Context context, Display display) { + super(context, display); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + // Be sure to call the super class. + super.onCreate(savedInstanceState); + + // Inflate the layout. + setContentView(R.layout.sample_media_router_presentation); + + // Set up the surface view. + mPresentationSurfaceView = (SurfaceView)findViewById(R.id.surface_view); + SurfaceHolder holder = mPresentationSurfaceView.getHolder(); + holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); + holder.addCallback(SurfaceViewPlayer.this); + Log.i(TAG, "Presentation created"); + } + + public void updateSize(int width, int height) { + int surfaceHeight = getWindow().getDecorView().getHeight(); + int surfaceWidth = getWindow().getDecorView().getWidth(); + ViewGroup.LayoutParams lp = mPresentationSurfaceView.getLayoutParams(); + if (surfaceWidth * height < surfaceHeight * width) { + lp.width = surfaceWidth; + lp.height = surfaceWidth * height / width; + } else { + lp.width = surfaceHeight * width / height; + lp.height = surfaceHeight; + } + Log.i(TAG, "Presentation video rect is " + lp.width + "x" + lp.height); + mPresentationSurfaceView.setLayoutParams(lp); + } + } + } + + /** + * Handles playback of a single media item using MediaPlayer in + * OverlayDisplayWindow. + */ + public static class OverlayPlayer extends LocalPlayer implements + OverlayDisplayWindow.OverlayWindowListener { + private static final String TAG = "OverlayPlayer"; + private final OverlayDisplayWindow mOverlay; + + public OverlayPlayer(Context context) { + super(context); + + mOverlay = OverlayDisplayWindow.create(getContext(), + getContext().getResources().getString( + R.string.sample_media_route_provider_remote), + 1024, 768, Gravity.CENTER); + + mOverlay.setOverlayWindowListener(this); + } + + @Override + public void connect(RouteInfo route) { + super.connect(route); + mOverlay.show(); + } + + @Override + public void release() { + super.release(); + mOverlay.dismiss(); + } + + @Override + protected void updateSize() { + int width = getVideoWidth(); + int height = getVideoHeight(); + if (width > 0 && height > 0) { + mOverlay.updateAspectRatio(width, height); + } + } + + // OverlayDisplayWindow.OverlayWindowListener + @Override + public void onWindowCreated(Surface surface) { + setSurface(surface); + } + + @Override + public void onWindowCreated(SurfaceHolder surfaceHolder) { + setSurface(surfaceHolder); + } + + @Override + public void onWindowDestroyed() { + setSurface((SurfaceHolder)null); + } + } +} diff --git a/samples/browseable/MediaRouter/src/com.example.android.mediarouter/player/MainActivity.java b/samples/browseable/MediaRouter/src/com.example.android.mediarouter/player/MainActivity.java new file mode 100644 index 000000000..ad283dd24 --- /dev/null +++ b/samples/browseable/MediaRouter/src/com.example.android.mediarouter/player/MainActivity.java @@ -0,0 +1,724 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.mediarouter.player; + +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.media.AudioManager; +import android.media.AudioManager.OnAudioFocusChangeListener; +import android.media.RemoteControlClient; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Environment; +import android.os.Handler; +import android.os.SystemClock; +import android.support.v4.app.FragmentManager; +import android.support.v4.view.MenuItemCompat; +import android.support.v7.app.ActionBarActivity; +import android.support.v7.app.MediaRouteActionProvider; +import android.support.v7.app.MediaRouteDiscoveryFragment; +import android.support.v7.media.MediaControlIntent; +import android.support.v7.media.MediaItemStatus; +import android.support.v7.media.MediaRouteSelector; +import android.support.v7.media.MediaRouter; +import android.support.v7.media.MediaRouter.Callback; +import android.support.v7.media.MediaRouter.ProviderInfo; +import android.support.v7.media.MediaRouter.RouteInfo; +import android.util.Log; +import android.view.KeyEvent; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.ArrayAdapter; +import android.widget.ImageButton; +import android.widget.ListView; +import android.widget.SeekBar; +import android.widget.SeekBar.OnSeekBarChangeListener; +import android.widget.TabHost; +import android.widget.TabHost.OnTabChangeListener; +import android.widget.TabHost.TabSpec; +import android.widget.TextView; +import android.widget.Toast; + +import com.example.android.mediarouter.R; +import com.example.android.mediarouter.provider.SampleMediaRouteProvider; + +import java.io.File; + +/** + * <h3>Media Router Support Activity</h3> + * <p/> + * <p> + * This demonstrates how to use the {@link MediaRouter} API to build an + * application that allows the user to send content to various rendering + * targets. + * </p> + */ +public class MainActivity extends ActionBarActivity { + private static final String TAG = "MainActivity"; + private static final String DISCOVERY_FRAGMENT_TAG = "DiscoveryFragment"; + + private MediaRouter mMediaRouter; + private MediaRouteSelector mSelector; + private LibraryAdapter mLibraryItems; + private PlaylistAdapter mPlayListItems; + private TextView mInfoTextView; + private ListView mLibraryView; + private ListView mPlayListView; + private ImageButton mPauseResumeButton; + private ImageButton mStopButton; + private SeekBar mSeekBar; + private boolean mPaused; + private boolean mNeedResume; + private boolean mSeeking; + + private RemoteControlClient mRemoteControlClient; + private ComponentName mEventReceiver; + private AudioManager mAudioManager; + private PendingIntent mMediaPendingIntent; + + private final Handler mHandler = new Handler(); + private final Runnable mUpdateSeekRunnable = new Runnable() { + @Override + public void run() { + updateProgress(); + // update UI every 1 second + mHandler.postDelayed(this, 1000); + } + }; + + private final SessionManager mSessionManager = new SessionManager("app"); + private Player mPlayer; + + private final MediaRouter.Callback mMediaRouterCB = new MediaRouter.Callback() { + // Return a custom callback that will simply log all of the route events + // for demonstration purposes. + @Override + public void onRouteAdded(MediaRouter router, RouteInfo route) { + Log.d(TAG, "onRouteAdded: route=" + route); + } + + @Override + public void onRouteChanged(MediaRouter router, RouteInfo route) { + Log.d(TAG, "onRouteChanged: route=" + route); + } + + @Override + public void onRouteRemoved(MediaRouter router, RouteInfo route) { + Log.d(TAG, "onRouteRemoved: route=" + route); + } + + @Override + public void onRouteSelected(MediaRouter router, RouteInfo route) { + Log.d(TAG, "onRouteSelected: route=" + route); + + mPlayer = Player.create(MainActivity.this, route); + mPlayer.updatePresentation(); + mSessionManager.setPlayer(mPlayer); + mSessionManager.unsuspend(); + + registerRemoteControlClient(); + updateUi(); + } + + @Override + public void onRouteUnselected(MediaRouter router, RouteInfo route) { + Log.d(TAG, "onRouteUnselected: route=" + route); + unregisterRemoteControlClient(); + + PlaylistItem item = getCheckedPlaylistItem(); + if (item != null) { + long pos = item.getPosition() + + (mPaused ? 0 : (SystemClock.elapsedRealtime() - item.getTimestamp())); + mSessionManager.suspend(pos); + } + mPlayer.updatePresentation(); + mPlayer.release(); + } + + @Override + public void onRouteVolumeChanged(MediaRouter router, RouteInfo route) { + Log.d(TAG, "onRouteVolumeChanged: route=" + route); + } + + @Override + public void onRoutePresentationDisplayChanged(MediaRouter router, RouteInfo route) { + Log.d(TAG, "onRoutePresentationDisplayChanged: route=" + route); + mPlayer.updatePresentation(); + } + + @Override + public void onProviderAdded(MediaRouter router, ProviderInfo provider) { + Log.d(TAG, "onRouteProviderAdded: provider=" + provider); + } + + @Override + public void onProviderRemoved(MediaRouter router, ProviderInfo provider) { + Log.d(TAG, "onRouteProviderRemoved: provider=" + provider); + } + + @Override + public void onProviderChanged(MediaRouter router, ProviderInfo provider) { + Log.d(TAG, "onRouteProviderChanged: provider=" + provider); + } + }; + + private final OnAudioFocusChangeListener mAfChangeListener = new OnAudioFocusChangeListener() { + @Override + public void onAudioFocusChange(int focusChange) { + if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) { + Log.d(TAG, "onAudioFocusChange: LOSS_TRANSIENT"); + } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) { + Log.d(TAG, "onAudioFocusChange: AUDIOFOCUS_GAIN"); + } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS) { + Log.d(TAG, "onAudioFocusChange: AUDIOFOCUS_LOSS"); + } + } + }; + + @Override + protected void onCreate(Bundle savedInstanceState) { + // Be sure to call the super class. + super.onCreate(savedInstanceState); + if (savedInstanceState != null) { + mPlayer = (Player) savedInstanceState.getSerializable("mPlayer"); + } + + // Get the media router service. + mMediaRouter = MediaRouter.getInstance(this); + + // Create a route selector for the type of routes that we care about. + mSelector = + new MediaRouteSelector.Builder().addControlCategory(MediaControlIntent + .CATEGORY_LIVE_AUDIO).addControlCategory(MediaControlIntent + .CATEGORY_LIVE_VIDEO).addControlCategory(MediaControlIntent + .CATEGORY_REMOTE_PLAYBACK).addControlCategory(SampleMediaRouteProvider + .CATEGORY_SAMPLE_ROUTE).build(); + + // Add a fragment to take care of media route discovery. + // This fragment automatically adds or removes a callback whenever the activity + // is started or stopped. + FragmentManager fm = getSupportFragmentManager(); + DiscoveryFragment fragment = + (DiscoveryFragment) fm.findFragmentByTag(DISCOVERY_FRAGMENT_TAG); + if (fragment == null) { + fragment = new DiscoveryFragment(mMediaRouterCB); + fragment.setRouteSelector(mSelector); + fm.beginTransaction().add(fragment, DISCOVERY_FRAGMENT_TAG).commit(); + } else { + fragment.setCallback(mMediaRouterCB); + fragment.setRouteSelector(mSelector); + } + + // Populate an array adapter with streaming media items. + String[] mediaNames = getResources().getStringArray(R.array.media_names); + String[] mediaUris = getResources().getStringArray(R.array.media_uris); + mLibraryItems = new LibraryAdapter(); + for (int i = 0; i < mediaNames.length; i++) { + mLibraryItems.add(new MediaItem( + "[streaming] " + mediaNames[i], Uri.parse(mediaUris[i]), "video/mp4")); + } + + // Scan local external storage directory for media files. + File externalDir = Environment.getExternalStorageDirectory(); + if (externalDir != null) { + File list[] = externalDir.listFiles(); + if (list != null) { + for (int i = 0; i < list.length; i++) { + String filename = list[i].getName(); + if (filename.matches(".*\\.(m4v|mp4)")) { + mLibraryItems.add(new MediaItem( + "[local] " + filename, Uri.fromFile(list[i]), "video/mp4")); + } + } + } + } + + mPlayListItems = new PlaylistAdapter(); + + // Initialize the layout. + setContentView(R.layout.sample_media_router); + + TabHost tabHost = (TabHost) findViewById(R.id.tabHost); + tabHost.setup(); + String tabName = getResources().getString(R.string.library_tab_text); + TabSpec spec1 = tabHost.newTabSpec(tabName); + spec1.setContent(R.id.tab1); + spec1.setIndicator(tabName); + + tabName = getResources().getString(R.string.playlist_tab_text); + TabSpec spec2 = tabHost.newTabSpec(tabName); + spec2.setIndicator(tabName); + spec2.setContent(R.id.tab2); + + tabName = getResources().getString(R.string.statistics_tab_text); + TabSpec spec3 = tabHost.newTabSpec(tabName); + spec3.setIndicator(tabName); + spec3.setContent(R.id.tab3); + + tabHost.addTab(spec1); + tabHost.addTab(spec2); + tabHost.addTab(spec3); + tabHost.setOnTabChangedListener(new OnTabChangeListener() { + @Override + public void onTabChanged(String arg0) { + updateUi(); + } + }); + + mLibraryView = (ListView) findViewById(R.id.media); + mLibraryView.setAdapter(mLibraryItems); + mLibraryView.setChoiceMode(ListView.CHOICE_MODE_SINGLE); + mLibraryView.setOnItemClickListener(new OnItemClickListener() { + @Override + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { + updateButtons(); + } + }); + + mPlayListView = (ListView) findViewById(R.id.playlist); + mPlayListView.setAdapter(mPlayListItems); + mPlayListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE); + mPlayListView.setOnItemClickListener(new OnItemClickListener() { + @Override + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { + updateButtons(); + } + }); + + mInfoTextView = (TextView) findViewById(R.id.info); + + mPauseResumeButton = (ImageButton) findViewById(R.id.pause_resume_button); + mPauseResumeButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + mPaused = !mPaused; + if (mPaused) { + mSessionManager.pause(); + } else { + mSessionManager.resume(); + } + } + }); + + mStopButton = (ImageButton) findViewById(R.id.stop_button); + mStopButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + mPaused = false; + mSessionManager.stop(); + } + }); + + mSeekBar = (SeekBar) findViewById(R.id.seekbar); + mSeekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + PlaylistItem item = getCheckedPlaylistItem(); + if (fromUser && item != null && item.getDuration() > 0) { + long pos = progress * item.getDuration() / 100; + mSessionManager.seek(item.getItemId(), pos); + item.setPosition(pos); + item.setTimestamp(SystemClock.elapsedRealtime()); + } + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + mSeeking = true; + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + mSeeking = false; + updateUi(); + } + }); + + // Schedule Ui update + mHandler.postDelayed(mUpdateSeekRunnable, 1000); + + // Build the PendingIntent for the remote control client + mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); + mEventReceiver = + new ComponentName(getPackageName(), SampleMediaButtonReceiver.class.getName()); + Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON); + mediaButtonIntent.setComponent(mEventReceiver); + mMediaPendingIntent = PendingIntent.getBroadcast(this, 0, mediaButtonIntent, 0); + + // Create and register the remote control client + registerRemoteControlClient(); + + // Set up playback manager and player + mPlayer = Player.create(MainActivity.this, mMediaRouter.getSelectedRoute()); + mSessionManager.setPlayer(mPlayer); + mSessionManager.setCallback(new SessionManager.Callback() { + @Override + public void onStatusChanged() { + updateUi(); + } + + @Override + public void onItemChanged(PlaylistItem item) { + } + }); + + updateUi(); + } + + private void registerRemoteControlClient() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + // Create the RCC and register with AudioManager and MediaRouter + mAudioManager.requestAudioFocus(mAfChangeListener, AudioManager.STREAM_MUSIC, + AudioManager.AUDIOFOCUS_GAIN); + mAudioManager.registerMediaButtonEventReceiver(mEventReceiver); + mRemoteControlClient = new RemoteControlClient(mMediaPendingIntent); + mAudioManager.registerRemoteControlClient(mRemoteControlClient); + mMediaRouter.addRemoteControlClient(mRemoteControlClient); + SampleMediaButtonReceiver.setActivity(MainActivity.this); + mRemoteControlClient.setTransportControlFlags(RemoteControlClient + .FLAG_KEY_MEDIA_PLAY_PAUSE); + mRemoteControlClient.setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING); + } + } + + private void unregisterRemoteControlClient() { + // Unregister the RCC with AudioManager and MediaRouter + if (mRemoteControlClient != null) { + mRemoteControlClient.setTransportControlFlags(0); + mAudioManager.abandonAudioFocus(mAfChangeListener); + mAudioManager.unregisterMediaButtonEventReceiver(mEventReceiver); + mAudioManager.unregisterRemoteControlClient(mRemoteControlClient); + mMediaRouter.removeRemoteControlClient(mRemoteControlClient); + SampleMediaButtonReceiver.setActivity(null); + mRemoteControlClient = null; + } + } + + public boolean handleMediaKey(KeyEvent event) { + if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) { + switch (event.getKeyCode()) { + case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: { + Log.d(TAG, "Received Play/Pause event from RemoteControlClient"); + mPaused = !mPaused; + if (mPaused) { + mSessionManager.pause(); + } else { + mSessionManager.resume(); + } + return true; + } + case KeyEvent.KEYCODE_MEDIA_PLAY: { + Log.d(TAG, "Received Play event from RemoteControlClient"); + if (mPaused) { + mPaused = false; + mSessionManager.resume(); + } + return true; + } + case KeyEvent.KEYCODE_MEDIA_PAUSE: { + Log.d(TAG, "Received Pause event from RemoteControlClient"); + if (!mPaused) { + mPaused = true; + mSessionManager.pause(); + } + return true; + } + case KeyEvent.KEYCODE_MEDIA_STOP: { + Log.d(TAG, "Received Stop event from RemoteControlClient"); + mPaused = false; + mSessionManager.stop(); + return true; + } + default: + break; + } + } + return false; + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + return handleMediaKey(event) || super.onKeyDown(keyCode, event); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + return handleMediaKey(event) || super.onKeyUp(keyCode, event); + } + + @Override + public void onStart() { + // Be sure to call the super class. + super.onStart(); + } + + @Override + public void onPause() { + // pause media player for local playback case only + if (!mPlayer.isRemotePlayback() && !mPaused) { + mNeedResume = true; + mSessionManager.pause(); + } + super.onPause(); + } + + @Override + public void onResume() { + // resume media player for local playback case only + if (!mPlayer.isRemotePlayback() && mNeedResume) { + mSessionManager.resume(); + mNeedResume = false; + } + super.onResume(); + } + + @Override + public void onDestroy() { + // Unregister the remote control client + unregisterRemoteControlClient(); + + mPaused = false; + mSessionManager.stop(); + mPlayer.release(); + super.onDestroy(); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + // Be sure to call the super class. + super.onCreateOptionsMenu(menu); + + // Inflate the menu and configure the media router action provider. + getMenuInflater().inflate(R.menu.sample_media_router_menu, menu); + + MenuItem mediaRouteMenuItem = menu.findItem(R.id.media_route_menu_item); + MediaRouteActionProvider mediaRouteActionProvider = + (MediaRouteActionProvider) MenuItemCompat.getActionProvider(mediaRouteMenuItem); + mediaRouteActionProvider.setRouteSelector(mSelector); + + // Return true to show the menu. + return true; + } + + private void updateProgress() { + // Estimate content position from last status time and elapsed time. + // (Note this might be slightly out of sync with remote side, however + // it avoids frequent polling the MRP.) + int progress = 0; + PlaylistItem item = getCheckedPlaylistItem(); + if (item != null) { + int state = item.getState(); + long duration = item.getDuration(); + if (duration <= 0) { + if (state == MediaItemStatus.PLAYBACK_STATE_PLAYING || + state == MediaItemStatus.PLAYBACK_STATE_PAUSED) { + mSessionManager.updateStatus(); + } + } else { + long position = item.getPosition(); + long timeDelta = + mPaused ? 0 : (SystemClock.elapsedRealtime() - item.getTimestamp()); + progress = (int) (100.0 * (position + timeDelta) / duration); + } + } + mSeekBar.setProgress(progress); + } + + private void updateUi() { + updatePlaylist(); + updateRouteDescription(); + updateButtons(); + } + + private void updatePlaylist() { + mPlayListItems.clear(); + for (PlaylistItem item : mSessionManager.getPlaylist()) { + mPlayListItems.add(item); + } + mPlayListView.invalidate(); + } + + + private void updateRouteDescription() { + RouteInfo route = mMediaRouter.getSelectedRoute(); + mInfoTextView.setText( + "Currently selected route:" + "\nName: " + route.getName() + "\nProvider: " + + route.getProvider().getPackageName() + "\nDescription: " + + route.getDescription() + "\nStatistics: " + + mSessionManager.getStatistics()); + } + + private void updateButtons() { + MediaRouter.RouteInfo route = mMediaRouter.getSelectedRoute(); + // show pause or resume icon depending on current state + mPauseResumeButton.setImageResource( + mPaused ? R.drawable.ic_action_play : R.drawable.ic_action_pause); + // disable pause/resume/stop if no session + mPauseResumeButton.setEnabled(mSessionManager.hasSession()); + mStopButton.setEnabled(mSessionManager.hasSession()); + // only enable seek bar when duration is known + PlaylistItem item = getCheckedPlaylistItem(); + mSeekBar.setEnabled(item != null && item.getDuration() > 0); + if (mRemoteControlClient != null) { + mRemoteControlClient.setPlaybackState(mPaused ? RemoteControlClient.PLAYSTATE_PAUSED : + RemoteControlClient.PLAYSTATE_PLAYING); + } + } + + private PlaylistItem getCheckedPlaylistItem() { + int count = mPlayListView.getCount(); + int index = mPlayListView.getCheckedItemPosition(); + if (count > 0) { + if (index < 0 || index >= count) { + index = 0; + mPlayListView.setItemChecked(0, true); + } + return mPlayListItems.getItem(index); + } + return null; + } + + public static final class DiscoveryFragment extends MediaRouteDiscoveryFragment { + private static final String TAG = "DiscoveryFragment"; + private Callback mCallback; + + public DiscoveryFragment() { + mCallback = null; + } + + public DiscoveryFragment(Callback cb) { + mCallback = cb; + } + + public void setCallback(Callback cb) { + mCallback = cb; + } + + @Override + public Callback onCreateCallback() { + return mCallback; + } + + @Override + public int onPrepareCallbackFlags() { + // Add the CALLBACK_FLAG_UNFILTERED_EVENTS flag to ensure that we will + // observe and log all route events including those that are for routes + // that do not match our selector. This is only for demonstration purposes + // and should not be needed by most applications. + return super.onPrepareCallbackFlags() | MediaRouter.CALLBACK_FLAG_UNFILTERED_EVENTS; + } + } + + private static final class MediaItem { + public final String mName; + public final Uri mUri; + public final String mMime; + + public MediaItem(String name, Uri uri, String mime) { + mName = name; + mUri = uri; + mMime = mime; + } + + @Override + public String toString() { + return mName; + } + } + + private final class LibraryAdapter extends ArrayAdapter<MediaItem> { + public LibraryAdapter() { + super(MainActivity.this, R.layout.media_item); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + final View v; + if (convertView == null) { + v = getLayoutInflater().inflate(R.layout.media_item, null); + } else { + v = convertView; + } + + final MediaItem item = getItem(position); + + TextView tv = (TextView) v.findViewById(R.id.item_text); + tv.setText(item.mName); + + ImageButton b = (ImageButton) v.findViewById(R.id.item_action); + b.setImageResource(R.drawable.ic_suggestions_add); + b.setTag(item); + b.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (item != null) { + mSessionManager.add(item.mUri, item.mMime); + Toast.makeText(MainActivity.this, R.string.playlist_item_added_text, + Toast.LENGTH_SHORT).show(); + } + } + }); + + return v; + } + } + + private final class PlaylistAdapter extends ArrayAdapter<PlaylistItem> { + public PlaylistAdapter() { + super(MainActivity.this, R.layout.media_item); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + final View v; + if (convertView == null) { + v = getLayoutInflater().inflate(R.layout.media_item, null); + } else { + v = convertView; + } + + final PlaylistItem item = getItem(position); + + TextView tv = (TextView) v.findViewById(R.id.item_text); + tv.setText(item.toString()); + + ImageButton b = (ImageButton) v.findViewById(R.id.item_action); + b.setImageResource(R.drawable.ic_suggestions_delete); + b.setTag(item); + b.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (item != null) { + mSessionManager.remove(item.getItemId()); + Toast.makeText(MainActivity.this, R.string.playlist_item_removed_text, + Toast.LENGTH_SHORT).show(); + } + } + }); + + return v; + } + } +} diff --git a/samples/browseable/MediaRouter/src/com.example.android.mediarouter/player/OverlayDisplayWindow.java b/samples/browseable/MediaRouter/src/com.example.android.mediarouter/player/OverlayDisplayWindow.java new file mode 100644 index 000000000..58348308e --- /dev/null +++ b/samples/browseable/MediaRouter/src/com.example.android.mediarouter/player/OverlayDisplayWindow.java @@ -0,0 +1,463 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.mediarouter.player; + +import android.content.Context; +import android.graphics.SurfaceTexture; +import android.hardware.display.DisplayManager; +import android.os.Build; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.Display; +import android.view.GestureDetector; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.ScaleGestureDetector; +import android.view.Surface; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.TextureView; +import android.view.TextureView.SurfaceTextureListener; +import android.view.View; +import android.view.WindowManager; +import android.widget.TextView; + +import com.example.android.mediarouter.R; + +/** + * Manages an overlay display window, used for simulating remote playback. + */ +public abstract class OverlayDisplayWindow { + private static final String TAG = "OverlayDisplayWindow"; + private static final boolean DEBUG = false; + + private static final float WINDOW_ALPHA = 0.8f; + private static final float INITIAL_SCALE = 0.5f; + private static final float MIN_SCALE = 0.3f; + private static final float MAX_SCALE = 1.0f; + + protected final Context mContext; + protected final String mName; + protected final int mWidth; + protected final int mHeight; + protected final int mGravity; + protected OverlayWindowListener mListener; + + protected OverlayDisplayWindow(Context context, String name, + int width, int height, int gravity) { + mContext = context; + mName = name; + mWidth = width; + mHeight = height; + mGravity = gravity; + } + + public static OverlayDisplayWindow create(Context context, String name, + int width, int height, int gravity) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + return new JellybeanMr1Impl(context, name, width, height, gravity); + } else { + return new LegacyImpl(context, name, width, height, gravity); + } + } + + public void setOverlayWindowListener(OverlayWindowListener listener) { + mListener = listener; + } + + public Context getContext() { + return mContext; + } + + public abstract void show(); + + public abstract void dismiss(); + + public abstract void updateAspectRatio(int width, int height); + + // Watches for significant changes in the overlay display window lifecycle. + public interface OverlayWindowListener { + public void onWindowCreated(Surface surface); + public void onWindowCreated(SurfaceHolder surfaceHolder); + public void onWindowDestroyed(); + } + + /** + * Implementation for older versions. + */ + private static final class LegacyImpl extends OverlayDisplayWindow { + private final WindowManager mWindowManager; + + private boolean mWindowVisible; + private SurfaceView mSurfaceView; + + public LegacyImpl(Context context, String name, + int width, int height, int gravity) { + super(context, name, width, height, gravity); + + mWindowManager = (WindowManager)context.getSystemService( + Context.WINDOW_SERVICE); + } + + @Override + public void show() { + if (!mWindowVisible) { + mSurfaceView = new SurfaceView(mContext); + + Display display = mWindowManager.getDefaultDisplay(); + + WindowManager.LayoutParams params = new WindowManager.LayoutParams( + WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); + params.flags |= WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN + | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS + | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL + | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; + params.alpha = WINDOW_ALPHA; + params.gravity = Gravity.LEFT | Gravity.BOTTOM; + params.setTitle(mName); + + int width = (int)(display.getWidth() * INITIAL_SCALE); + int height = (int)(display.getHeight() * INITIAL_SCALE); + if (mWidth > mHeight) { + height = mHeight * width / mWidth; + } else { + width = mWidth * height / mHeight; + } + params.width = width; + params.height = height; + + mWindowManager.addView(mSurfaceView, params); + mWindowVisible = true; + + SurfaceHolder holder = mSurfaceView.getHolder(); + holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); + mListener.onWindowCreated(holder); + } + } + + @Override + public void dismiss() { + if (mWindowVisible) { + mListener.onWindowDestroyed(); + + mWindowManager.removeView(mSurfaceView); + mWindowVisible = false; + } + } + + @Override + public void updateAspectRatio(int width, int height) { + } + } + + /** + * Implementation for API version 17+. + */ + private static final class JellybeanMr1Impl extends OverlayDisplayWindow { + // When true, disables support for moving and resizing the overlay. + // The window is made non-touchable, which makes it possible to + // directly interact with the content underneath. + private static final boolean DISABLE_MOVE_AND_RESIZE = false; + + private final DisplayManager mDisplayManager; + private final WindowManager mWindowManager; + + private final Display mDefaultDisplay; + private final DisplayMetrics mDefaultDisplayMetrics = new DisplayMetrics(); + + private View mWindowContent; + private WindowManager.LayoutParams mWindowParams; + private TextureView mTextureView; + private TextView mNameTextView; + + private GestureDetector mGestureDetector; + private ScaleGestureDetector mScaleGestureDetector; + + private boolean mWindowVisible; + private int mWindowX; + private int mWindowY; + private float mWindowScale; + + private float mLiveTranslationX; + private float mLiveTranslationY; + private float mLiveScale = 1.0f; + + public JellybeanMr1Impl(Context context, String name, + int width, int height, int gravity) { + super(context, name, width, height, gravity); + + mDisplayManager = (DisplayManager)context.getSystemService( + Context.DISPLAY_SERVICE); + mWindowManager = (WindowManager)context.getSystemService( + Context.WINDOW_SERVICE); + + mDefaultDisplay = mWindowManager.getDefaultDisplay(); + updateDefaultDisplayInfo(); + + createWindow(); + } + + @Override + public void show() { + if (!mWindowVisible) { + mDisplayManager.registerDisplayListener(mDisplayListener, null); + if (!updateDefaultDisplayInfo()) { + mDisplayManager.unregisterDisplayListener(mDisplayListener); + return; + } + + clearLiveState(); + updateWindowParams(); + mWindowManager.addView(mWindowContent, mWindowParams); + mWindowVisible = true; + } + } + + @Override + public void dismiss() { + if (mWindowVisible) { + mDisplayManager.unregisterDisplayListener(mDisplayListener); + mWindowManager.removeView(mWindowContent); + mWindowVisible = false; + } + } + + @Override + public void updateAspectRatio(int width, int height) { + if (mWidth * height < mHeight * width) { + mTextureView.getLayoutParams().width = mWidth; + mTextureView.getLayoutParams().height = mWidth * height / width; + } else { + mTextureView.getLayoutParams().width = mHeight * width / height; + mTextureView.getLayoutParams().height = mHeight; + } + relayout(); + } + + private void relayout() { + if (mWindowVisible) { + updateWindowParams(); + mWindowManager.updateViewLayout(mWindowContent, mWindowParams); + } + } + + private boolean updateDefaultDisplayInfo() { + mDefaultDisplay.getMetrics(mDefaultDisplayMetrics); + return true; + } + + private void createWindow() { + LayoutInflater inflater = LayoutInflater.from(mContext); + + mWindowContent = inflater.inflate( + R.layout.overlay_display_window, null); + mWindowContent.setOnTouchListener(mOnTouchListener); + + mTextureView = (TextureView)mWindowContent.findViewById( + R.id.overlay_display_window_texture); + mTextureView.setPivotX(0); + mTextureView.setPivotY(0); + mTextureView.getLayoutParams().width = mWidth; + mTextureView.getLayoutParams().height = mHeight; + mTextureView.setOpaque(false); + mTextureView.setSurfaceTextureListener(mSurfaceTextureListener); + + mNameTextView = (TextView)mWindowContent.findViewById( + R.id.overlay_display_window_title); + mNameTextView.setText(mName); + + mWindowParams = new WindowManager.LayoutParams( + WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); + mWindowParams.flags |= WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN + | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS + | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL + | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED; + if (DISABLE_MOVE_AND_RESIZE) { + mWindowParams.flags |= WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; + } + mWindowParams.alpha = WINDOW_ALPHA; + mWindowParams.gravity = Gravity.TOP | Gravity.LEFT; + mWindowParams.setTitle(mName); + + mGestureDetector = new GestureDetector(mContext, mOnGestureListener); + mScaleGestureDetector = new ScaleGestureDetector(mContext, mOnScaleGestureListener); + + // Set the initial position and scale. + // The position and scale will be clamped when the display is first shown. + mWindowX = (mGravity & Gravity.LEFT) == Gravity.LEFT ? + 0 : mDefaultDisplayMetrics.widthPixels; + mWindowY = (mGravity & Gravity.TOP) == Gravity.TOP ? + 0 : mDefaultDisplayMetrics.heightPixels; + Log.d(TAG, mDefaultDisplayMetrics.toString()); + mWindowScale = INITIAL_SCALE; + + // calculate and save initial settings + updateWindowParams(); + saveWindowParams(); + } + + private void updateWindowParams() { + float scale = mWindowScale * mLiveScale; + scale = Math.min(scale, (float)mDefaultDisplayMetrics.widthPixels / mWidth); + scale = Math.min(scale, (float)mDefaultDisplayMetrics.heightPixels / mHeight); + scale = Math.max(MIN_SCALE, Math.min(MAX_SCALE, scale)); + + float offsetScale = (scale / mWindowScale - 1.0f) * 0.5f; + int width = (int)(mWidth * scale); + int height = (int)(mHeight * scale); + int x = (int)(mWindowX + mLiveTranslationX - width * offsetScale); + int y = (int)(mWindowY + mLiveTranslationY - height * offsetScale); + x = Math.max(0, Math.min(x, mDefaultDisplayMetrics.widthPixels - width)); + y = Math.max(0, Math.min(y, mDefaultDisplayMetrics.heightPixels - height)); + + if (DEBUG) { + Log.d(TAG, "updateWindowParams: scale=" + scale + + ", offsetScale=" + offsetScale + + ", x=" + x + ", y=" + y + + ", width=" + width + ", height=" + height); + } + + mTextureView.setScaleX(scale); + mTextureView.setScaleY(scale); + + mTextureView.setTranslationX( + (mWidth - mTextureView.getLayoutParams().width) * scale / 2); + mTextureView.setTranslationY( + (mHeight - mTextureView.getLayoutParams().height) * scale / 2); + + mWindowParams.x = x; + mWindowParams.y = y; + mWindowParams.width = width; + mWindowParams.height = height; + } + + private void saveWindowParams() { + mWindowX = mWindowParams.x; + mWindowY = mWindowParams.y; + mWindowScale = mTextureView.getScaleX(); + clearLiveState(); + } + + private void clearLiveState() { + mLiveTranslationX = 0f; + mLiveTranslationY = 0f; + mLiveScale = 1.0f; + } + + private final DisplayManager.DisplayListener mDisplayListener = + new DisplayManager.DisplayListener() { + @Override + public void onDisplayAdded(int displayId) { + } + + @Override + public void onDisplayChanged(int displayId) { + if (displayId == mDefaultDisplay.getDisplayId()) { + if (updateDefaultDisplayInfo()) { + relayout(); + } else { + dismiss(); + } + } + } + + @Override + public void onDisplayRemoved(int displayId) { + if (displayId == mDefaultDisplay.getDisplayId()) { + dismiss(); + } + } + }; + + private final SurfaceTextureListener mSurfaceTextureListener = + new SurfaceTextureListener() { + @Override + public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, + int width, int height) { + if (mListener != null) { + mListener.onWindowCreated(new Surface(surfaceTexture)); + } + } + + @Override + public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) { + if (mListener != null) { + mListener.onWindowDestroyed(); + } + return true; + } + + @Override + public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, + int width, int height) { + } + + @Override + public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) { + } + }; + + private final View.OnTouchListener mOnTouchListener = new View.OnTouchListener() { + @Override + public boolean onTouch(View view, MotionEvent event) { + // Work in screen coordinates. + final float oldX = event.getX(); + final float oldY = event.getY(); + event.setLocation(event.getRawX(), event.getRawY()); + + mGestureDetector.onTouchEvent(event); + mScaleGestureDetector.onTouchEvent(event); + + switch (event.getActionMasked()) { + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + saveWindowParams(); + break; + } + + // Revert to window coordinates. + event.setLocation(oldX, oldY); + return true; + } + }; + + private final GestureDetector.OnGestureListener mOnGestureListener = + new GestureDetector.SimpleOnGestureListener() { + @Override + public boolean onScroll(MotionEvent e1, MotionEvent e2, + float distanceX, float distanceY) { + mLiveTranslationX -= distanceX; + mLiveTranslationY -= distanceY; + relayout(); + return true; + } + }; + + private final ScaleGestureDetector.OnScaleGestureListener mOnScaleGestureListener = + new ScaleGestureDetector.SimpleOnScaleGestureListener() { + @Override + public boolean onScale(ScaleGestureDetector detector) { + mLiveScale *= detector.getScaleFactor(); + relayout(); + return true; + } + }; + } +} diff --git a/samples/browseable/MediaRouter/src/com.example.android.mediarouter/player/Player.java b/samples/browseable/MediaRouter/src/com.example.android.mediarouter/player/Player.java new file mode 100644 index 000000000..f842cf66f --- /dev/null +++ b/samples/browseable/MediaRouter/src/com.example.android.mediarouter/player/Player.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.mediarouter.player; + +import android.content.Context; +import android.support.v7.media.MediaControlIntent; +import android.support.v7.media.MediaRouter.RouteInfo; + +/** + * Abstraction of common playback operations of media items, such as play, + * seek, etc. Used by PlaybackManager as a backend to handle actual playback + * of media items. + */ +public abstract class Player { + protected Callback mCallback; + + public abstract boolean isRemotePlayback(); + public abstract boolean isQueuingSupported(); + + public abstract void connect(RouteInfo route); + public abstract void release(); + + // basic operations that are always supported + public abstract void play(final PlaylistItem item); + public abstract void seek(final PlaylistItem item); + public abstract void getStatus(final PlaylistItem item, final boolean update); + public abstract void pause(); + public abstract void resume(); + public abstract void stop(); + + // advanced queuing (enqueue & remove) are only supported + // if isQueuingSupported() returns true + public abstract void enqueue(final PlaylistItem item); + public abstract PlaylistItem remove(String iid); + + // route statistics + public void updateStatistics() {} + public String getStatistics() { return ""; } + + // presentation display + public void updatePresentation() {} + + public void setCallback(Callback callback) { + mCallback = callback; + } + + public static Player create(Context context, RouteInfo route) { + Player player; + if (route != null && route.supportsControlCategory( + MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)) { + player = new RemotePlayer(context); + } else if (route != null) { + player = new LocalPlayer.SurfaceViewPlayer(context); + } else { + player = new LocalPlayer.OverlayPlayer(context); + } + player.connect(route); + return player; + } + + public interface Callback { + void onError(); + void onCompletion(); + void onPlaylistChanged(); + void onPlaylistReady(); + } +} diff --git a/samples/browseable/MediaRouter/src/com.example.android.mediarouter/player/PlaylistItem.java b/samples/browseable/MediaRouter/src/com.example.android.mediarouter/player/PlaylistItem.java new file mode 100644 index 000000000..a5605384a --- /dev/null +++ b/samples/browseable/MediaRouter/src/com.example.android.mediarouter/player/PlaylistItem.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.mediarouter.player; + +import android.app.PendingIntent; +import android.net.Uri; +import android.os.SystemClock; +import android.support.v7.media.MediaItemStatus; + +/** + * PlaylistItem helps keep track of the current status of an media item. + */ +public final class PlaylistItem { + // immutables + private final String mSessionId; + private final String mItemId; + private final Uri mUri; + private final String mMime; + private final PendingIntent mUpdateReceiver; + // changeable states + private int mPlaybackState = MediaItemStatus.PLAYBACK_STATE_PENDING; + private long mContentPosition; + private long mContentDuration; + private long mTimestamp; + private String mRemoteItemId; + + public PlaylistItem(String qid, String iid, Uri uri, String mime, PendingIntent pi) { + mSessionId = qid; + mItemId = iid; + mUri = uri; + mMime = mime; + mUpdateReceiver = pi; + setTimestamp(SystemClock.elapsedRealtime()); + } + + public void setRemoteItemId(String riid) { + mRemoteItemId = riid; + } + + public void setState(int state) { + mPlaybackState = state; + } + + public void setPosition(long pos) { + mContentPosition = pos; + } + + public void setTimestamp(long ts) { + mTimestamp = ts; + } + + public void setDuration(long duration) { + mContentDuration = duration; + } + + public String getSessionId() { + return mSessionId; + } + + public String getItemId() { + return mItemId; + } + + public String getRemoteItemId() { + return mRemoteItemId; + } + + public Uri getUri() { + return mUri; + } + + public PendingIntent getUpdateReceiver() { + return mUpdateReceiver; + } + + public int getState() { + return mPlaybackState; + } + + public long getPosition() { + return mContentPosition; + } + + public long getDuration() { + return mContentDuration; + } + + public long getTimestamp() { + return mTimestamp; + } + + public MediaItemStatus getStatus() { + return new MediaItemStatus.Builder(mPlaybackState) + .setContentPosition(mContentPosition) + .setContentDuration(mContentDuration) + .setTimestamp(mTimestamp) + .build(); + } + + @Override + public String toString() { + String state[] = { + "PENDING", + "PLAYING", + "PAUSED", + "BUFFERING", + "FINISHED", + "CANCELED", + "INVALIDATED", + "ERROR" + }; + return "[" + mSessionId + "|" + mItemId + "|" + + (mRemoteItemId != null ? mRemoteItemId : "-") + "|" + + state[mPlaybackState] + "] " + mUri.toString(); + } +} diff --git a/samples/browseable/MediaRouter/src/com.example.android.mediarouter/player/RemotePlayer.java b/samples/browseable/MediaRouter/src/com.example.android.mediarouter/player/RemotePlayer.java new file mode 100644 index 000000000..672671887 --- /dev/null +++ b/samples/browseable/MediaRouter/src/com.example.android.mediarouter/player/RemotePlayer.java @@ -0,0 +1,482 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.mediarouter.player; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.support.v7.media.MediaItemStatus; +import android.support.v7.media.MediaRouter.ControlRequestCallback; +import android.support.v7.media.MediaRouter.RouteInfo; +import android.support.v7.media.MediaSessionStatus; +import android.support.v7.media.RemotePlaybackClient; +import android.support.v7.media.RemotePlaybackClient.ItemActionCallback; +import android.support.v7.media.RemotePlaybackClient.SessionActionCallback; +import android.support.v7.media.RemotePlaybackClient.StatusCallback; +import android.util.Log; + +import com.example.android.mediarouter.player.Player; +import com.example.android.mediarouter.player.PlaylistItem; +import com.example.android.mediarouter.provider.SampleMediaRouteProvider; + +import java.util.ArrayList; +import java.util.List; + +/** + * Handles playback of media items using a remote route. + * + * This class is used as a backend by PlaybackManager to feed media items to + * the remote route. When the remote route doesn't support queuing, media items + * are fed one-at-a-time; otherwise media items are enqueued to the remote side. + */ +public class RemotePlayer extends Player { + private static final String TAG = "RemotePlayer"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + private Context mContext; + private RouteInfo mRoute; + private boolean mEnqueuePending; + private String mStatsInfo = ""; + private List<PlaylistItem> mTempQueue = new ArrayList<PlaylistItem>(); + + private RemotePlaybackClient mClient; + private StatusCallback mStatusCallback = new StatusCallback() { + @Override + public void onItemStatusChanged(Bundle data, + String sessionId, MediaSessionStatus sessionStatus, + String itemId, MediaItemStatus itemStatus) { + logStatus("onItemStatusChanged", sessionId, sessionStatus, itemId, itemStatus); + if (mCallback != null) { + if (itemStatus.getPlaybackState() == + MediaItemStatus.PLAYBACK_STATE_FINISHED) { + mCallback.onCompletion(); + } else if (itemStatus.getPlaybackState() == + MediaItemStatus.PLAYBACK_STATE_ERROR) { + mCallback.onError(); + } + } + } + + @Override + public void onSessionStatusChanged(Bundle data, + String sessionId, MediaSessionStatus sessionStatus) { + logStatus("onSessionStatusChanged", sessionId, sessionStatus, null, null); + if (mCallback != null) { + mCallback.onPlaylistChanged(); + } + } + + @Override + public void onSessionChanged(String sessionId) { + if (DEBUG) { + Log.d(TAG, "onSessionChanged: sessionId=" + sessionId); + } + } + }; + + public RemotePlayer(Context context) { + mContext = context; + } + + @Override + public boolean isRemotePlayback() { + return true; + } + + @Override + public boolean isQueuingSupported() { + return mClient.isQueuingSupported(); + } + + @Override + public void connect(RouteInfo route) { + mRoute = route; + mClient = new RemotePlaybackClient(mContext, route); + mClient.setStatusCallback(mStatusCallback); + + if (DEBUG) { + Log.d(TAG, "connected to: " + route + + ", isRemotePlaybackSupported: " + mClient.isRemotePlaybackSupported() + + ", isQueuingSupported: "+ mClient.isQueuingSupported()); + } + } + + @Override + public void release() { + mClient.release(); + + if (DEBUG) { + Log.d(TAG, "released."); + } + } + + // basic playback operations that are always supported + @Override + public void play(final PlaylistItem item) { + if (DEBUG) { + Log.d(TAG, "play: item=" + item); + } + mClient.play(item.getUri(), "video/mp4", null, 0, null, new ItemActionCallback() { + @Override + public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus, + String itemId, MediaItemStatus itemStatus) { + logStatus("play: succeeded", sessionId, sessionStatus, itemId, itemStatus); + item.setRemoteItemId(itemId); + if (item.getPosition() > 0) { + seekInternal(item); + } + if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED) { + pause(); + } + if (mCallback != null) { + mCallback.onPlaylistChanged(); + } + } + + @Override + public void onError(String error, int code, Bundle data) { + logError("play: failed", error, code); + } + }); + } + + @Override + public void seek(final PlaylistItem item) { + seekInternal(item); + } + + @Override + public void getStatus(final PlaylistItem item, final boolean update) { + if (!mClient.hasSession() || item.getRemoteItemId() == null) { + // if session is not valid or item id not assigend yet. + // just return, it's not fatal + return; + } + + if (DEBUG) { + Log.d(TAG, "getStatus: item=" + item + ", update=" + update); + } + mClient.getStatus(item.getRemoteItemId(), null, new ItemActionCallback() { + @Override + public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus, + String itemId, MediaItemStatus itemStatus) { + logStatus("getStatus: succeeded", sessionId, sessionStatus, itemId, itemStatus); + int state = itemStatus.getPlaybackState(); + if (state == MediaItemStatus.PLAYBACK_STATE_PLAYING + || state == MediaItemStatus.PLAYBACK_STATE_PAUSED + || state == MediaItemStatus.PLAYBACK_STATE_PENDING) { + item.setState(state); + item.setPosition(itemStatus.getContentPosition()); + item.setDuration(itemStatus.getContentDuration()); + item.setTimestamp(itemStatus.getTimestamp()); + } + if (update && mCallback != null) { + mCallback.onPlaylistReady(); + } + } + + @Override + public void onError(String error, int code, Bundle data) { + logError("getStatus: failed", error, code); + if (update && mCallback != null) { + mCallback.onPlaylistReady(); + } + } + }); + } + + @Override + public void pause() { + if (!mClient.hasSession()) { + // ignore if no session + return; + } + if (DEBUG) { + Log.d(TAG, "pause"); + } + mClient.pause(null, new SessionActionCallback() { + @Override + public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) { + logStatus("pause: succeeded", sessionId, sessionStatus, null, null); + if (mCallback != null) { + mCallback.onPlaylistChanged(); + } + } + + @Override + public void onError(String error, int code, Bundle data) { + logError("pause: failed", error, code); + } + }); + } + + @Override + public void resume() { + if (!mClient.hasSession()) { + // ignore if no session + return; + } + if (DEBUG) { + Log.d(TAG, "resume"); + } + mClient.resume(null, new SessionActionCallback() { + @Override + public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) { + logStatus("resume: succeeded", sessionId, sessionStatus, null, null); + if (mCallback != null) { + mCallback.onPlaylistChanged(); + } + } + + @Override + public void onError(String error, int code, Bundle data) { + logError("resume: failed", error, code); + } + }); + } + + @Override + public void stop() { + if (!mClient.hasSession()) { + // ignore if no session + return; + } + if (DEBUG) { + Log.d(TAG, "stop"); + } + mClient.stop(null, new SessionActionCallback() { + @Override + public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) { + logStatus("stop: succeeded", sessionId, sessionStatus, null, null); + if (mClient.isSessionManagementSupported()) { + endSession(); + } + if (mCallback != null) { + mCallback.onPlaylistChanged(); + } + } + + @Override + public void onError(String error, int code, Bundle data) { + logError("stop: failed", error, code); + } + }); + } + + // enqueue & remove are only supported if isQueuingSupported() returns true + @Override + public void enqueue(final PlaylistItem item) { + throwIfQueuingUnsupported(); + + if (!mClient.hasSession() && !mEnqueuePending) { + mEnqueuePending = true; + if (mClient.isSessionManagementSupported()) { + startSession(item); + } else { + enqueueInternal(item); + } + } else if (mEnqueuePending){ + mTempQueue.add(item); + } else { + enqueueInternal(item); + } + } + + @Override + public PlaylistItem remove(String itemId) { + throwIfNoSession(); + throwIfQueuingUnsupported(); + + if (DEBUG) { + Log.d(TAG, "remove: itemId=" + itemId); + } + mClient.remove(itemId, null, new ItemActionCallback() { + @Override + public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus, + String itemId, MediaItemStatus itemStatus) { + logStatus("remove: succeeded", sessionId, sessionStatus, itemId, itemStatus); + } + + @Override + public void onError(String error, int code, Bundle data) { + logError("remove: failed", error, code); + } + }); + + return null; + } + + @Override + public void updateStatistics() { + // clear stats info first + mStatsInfo = ""; + + Intent intent = new Intent(SampleMediaRouteProvider.ACTION_GET_STATISTICS); + intent.addCategory(SampleMediaRouteProvider.CATEGORY_SAMPLE_ROUTE); + + if (mRoute != null && mRoute.supportsControlRequest(intent)) { + ControlRequestCallback callback = new ControlRequestCallback() { + @Override + public void onResult(Bundle data) { + if (DEBUG) { + Log.d(TAG, "getStatistics: succeeded: data=" + data); + } + if (data != null) { + int playbackCount = data.getInt( + SampleMediaRouteProvider.DATA_PLAYBACK_COUNT, -1); + mStatsInfo = "Total playback count: " + playbackCount; + } + } + + @Override + public void onError(String error, Bundle data) { + Log.d(TAG, "getStatistics: failed: error=" + error + ", data=" + data); + } + }; + + mRoute.sendControlRequest(intent, callback); + } + } + + @Override + public String getStatistics() { + return mStatsInfo; + } + + private void enqueueInternal(final PlaylistItem item) { + throwIfQueuingUnsupported(); + + if (DEBUG) { + Log.d(TAG, "enqueue: item=" + item); + } + mClient.enqueue(item.getUri(), "video/mp4", null, 0, null, new ItemActionCallback() { + @Override + public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus, + String itemId, MediaItemStatus itemStatus) { + logStatus("enqueue: succeeded", sessionId, sessionStatus, itemId, itemStatus); + item.setRemoteItemId(itemId); + if (item.getPosition() > 0) { + seekInternal(item); + } + if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED) { + pause(); + } + if (mEnqueuePending) { + mEnqueuePending = false; + for (PlaylistItem item : mTempQueue) { + enqueueInternal(item); + } + mTempQueue.clear(); + } + if (mCallback != null) { + mCallback.onPlaylistChanged(); + } + } + + @Override + public void onError(String error, int code, Bundle data) { + logError("enqueue: failed", error, code); + if (mCallback != null) { + mCallback.onPlaylistChanged(); + } + } + }); + } + + private void seekInternal(final PlaylistItem item) { + throwIfNoSession(); + + if (DEBUG) { + Log.d(TAG, "seek: item=" + item); + } + mClient.seek(item.getRemoteItemId(), item.getPosition(), null, new ItemActionCallback() { + @Override + public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus, + String itemId, MediaItemStatus itemStatus) { + logStatus("seek: succeeded", sessionId, sessionStatus, itemId, itemStatus); + if (mCallback != null) { + mCallback.onPlaylistChanged(); + } + } + + @Override + public void onError(String error, int code, Bundle data) { + logError("seek: failed", error, code); + } + }); + } + + private void startSession(final PlaylistItem item) { + mClient.startSession(null, new SessionActionCallback() { + @Override + public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) { + logStatus("startSession: succeeded", sessionId, sessionStatus, null, null); + enqueueInternal(item); + } + + @Override + public void onError(String error, int code, Bundle data) { + logError("startSession: failed", error, code); + } + }); + } + + private void endSession() { + mClient.endSession(null, new SessionActionCallback() { + @Override + public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) { + logStatus("endSession: succeeded", sessionId, sessionStatus, null, null); + } + + @Override + public void onError(String error, int code, Bundle data) { + logError("endSession: failed", error, code); + } + }); + } + + private void logStatus(String message, + String sessionId, MediaSessionStatus sessionStatus, + String itemId, MediaItemStatus itemStatus) { + if (DEBUG) { + String result = ""; + if (sessionId != null && sessionStatus != null) { + result += "sessionId=" + sessionId + ", sessionStatus=" + sessionStatus; + } + if (itemId != null & itemStatus != null) { + result += (result.isEmpty() ? "" : ", ") + + "itemId=" + itemId + ", itemStatus=" + itemStatus; + } + Log.d(TAG, message + ": " + result); + } + } + + private void logError(String message, String error, int code) { + Log.d(TAG, message + ": error=" + error + ", code=" + code); + } + + private void throwIfNoSession() { + if (!mClient.hasSession()) { + throw new IllegalStateException("Session is invalid"); + } + } + + private void throwIfQueuingUnsupported() { + if (!isQueuingSupported()) { + throw new UnsupportedOperationException("Queuing is unsupported"); + } + } +} diff --git a/samples/browseable/MediaRouter/src/com.example.android.mediarouter/player/SampleMediaButtonReceiver.java b/samples/browseable/MediaRouter/src/com.example.android.mediarouter/player/SampleMediaButtonReceiver.java new file mode 100644 index 000000000..855bc1eb7 --- /dev/null +++ b/samples/browseable/MediaRouter/src/com.example.android.mediarouter/player/SampleMediaButtonReceiver.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.mediarouter.player; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.view.KeyEvent; + +/** + * Broadcast receiver for handling ACTION_MEDIA_BUTTON. + * + * This is needed to create the RemoteControlClient for controlling + * remote route volume in lock screen. It routes media key events back + * to main app activity MainActivity. + */ +public class SampleMediaButtonReceiver extends BroadcastReceiver { + private static final String TAG = "SampleMediaButtonReceiver"; + private static MainActivity mActivity; + + public static void setActivity(MainActivity activity) { + mActivity = activity; + } + + @Override + public void onReceive(Context context, Intent intent) { + if (mActivity != null && Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())) { + mActivity.handleMediaKey( + (KeyEvent)intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT)); + } + } +} diff --git a/samples/browseable/MediaRouter/src/com.example.android.mediarouter/player/SessionManager.java b/samples/browseable/MediaRouter/src/com.example.android.mediarouter/player/SessionManager.java new file mode 100644 index 000000000..b6c5a46c2 --- /dev/null +++ b/samples/browseable/MediaRouter/src/com.example.android.mediarouter/player/SessionManager.java @@ -0,0 +1,419 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.mediarouter.player; + +import android.app.PendingIntent; +import android.net.Uri; +import android.support.v7.media.MediaItemStatus; +import android.support.v7.media.MediaSessionStatus; +import android.util.Log; + +import com.example.android.mediarouter.player.Player.Callback; + +import java.util.ArrayList; +import java.util.List; + +/** + * SessionManager manages a media session as a queue. It supports common + * queuing behaviors such as enqueue/remove of media items, pause/resume/stop, + * etc. + * + * Actual playback of a single media item is abstracted into a Player interface, + * and is handled outside this class. + */ +public class SessionManager implements Callback { + private static final String TAG = "SessionManager"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + private String mName; + private int mSessionId; + private int mItemId; + private boolean mPaused; + private boolean mSessionValid; + private Player mPlayer; + private Callback mCallback; + private List<PlaylistItem> mPlaylist = new ArrayList<PlaylistItem>(); + + public SessionManager(String name) { + mName = name; + } + + public boolean hasSession() { + return mSessionValid; + } + + public String getSessionId() { + return mSessionValid ? Integer.toString(mSessionId) : null; + } + + public PlaylistItem getCurrentItem() { + return mPlaylist.isEmpty() ? null : mPlaylist.get(0); + } + + // Get the cached statistic info from the player (will not update it) + public String getStatistics() { + checkPlayer(); + return mPlayer.getStatistics(); + } + + // Returns the cached playlist (note this is not responsible for updating it) + public List<PlaylistItem> getPlaylist() { + return mPlaylist; + } + + // Updates the playlist asynchronously, calls onPlaylistReady() when finished. + public void updateStatus() { + if (DEBUG) { + log("updateStatus"); + } + checkPlayer(); + // update the statistics first, so that the stats string is valid when + // onPlaylistReady() gets called in the end + mPlayer.updateStatistics(); + + if (mPlaylist.isEmpty()) { + // If queue is empty, don't forget to call onPlaylistReady()! + onPlaylistReady(); + } else if (mPlayer.isQueuingSupported()) { + // If player supports queuing, get status of each item. Player is + // responsible to call onPlaylistReady() after last getStatus(). + // (update=1 requires player to callback onPlaylistReady()) + for (int i = 0; i < mPlaylist.size(); i++) { + PlaylistItem item = mPlaylist.get(i); + mPlayer.getStatus(item, (i == mPlaylist.size() - 1) /* update */); + } + } else { + // Otherwise, only need to get status for current item. Player is + // responsible to call onPlaylistReady() when finished. + mPlayer.getStatus(getCurrentItem(), true /* update */); + } + } + + public PlaylistItem add(Uri uri, String mime) { + return add(uri, mime, null); + } + + public PlaylistItem add(Uri uri, String mime, PendingIntent receiver) { + if (DEBUG) { + log("add: uri=" + uri + ", receiver=" + receiver); + } + // create new session if needed + startSession(); + checkPlayerAndSession(); + + // append new item with initial status PLAYBACK_STATE_PENDING + PlaylistItem item = new PlaylistItem( + Integer.toString(mSessionId), Integer.toString(mItemId), uri, mime, receiver); + mPlaylist.add(item); + mItemId++; + + // if player supports queuing, enqueue the item now + if (mPlayer.isQueuingSupported()) { + mPlayer.enqueue(item); + } + updatePlaybackState(); + return item; + } + + public PlaylistItem remove(String iid) { + if (DEBUG) { + log("remove: iid=" + iid); + } + checkPlayerAndSession(); + return removeItem(iid, MediaItemStatus.PLAYBACK_STATE_CANCELED); + } + + public PlaylistItem seek(String iid, long pos) { + if (DEBUG) { + log("seek: iid=" + iid +", pos=" + pos); + } + checkPlayerAndSession(); + // seeking on pending items are not yet supported + checkItemCurrent(iid); + + PlaylistItem item = getCurrentItem(); + if (pos != item.getPosition()) { + item.setPosition(pos); + if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PLAYING + || item.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED) { + mPlayer.seek(item); + } + } + return item; + } + + public PlaylistItem getStatus(String iid) { + checkPlayerAndSession(); + + // This should only be called for local player. Remote player is + // asynchronous, need to use updateStatus() instead. + if (mPlayer.isRemotePlayback()) { + throw new IllegalStateException( + "getStatus should not be called on remote player!"); + } + + for (PlaylistItem item : mPlaylist) { + if (item.getItemId().equals(iid)) { + if (item == getCurrentItem()) { + mPlayer.getStatus(item, false); + } + return item; + } + } + return null; + } + + public void pause() { + if (DEBUG) { + log("pause"); + } + mPaused = true; + updatePlaybackState(); + } + + public void resume() { + if (DEBUG) { + log("resume"); + } + mPaused = false; + updatePlaybackState(); + } + + public void stop() { + if (DEBUG) { + log("stop"); + } + mPlayer.stop(); + mPlaylist.clear(); + mPaused = false; + updateStatus(); + } + + public String startSession() { + if (!mSessionValid) { + mSessionId++; + mItemId = 0; + mPaused = false; + mSessionValid = true; + return Integer.toString(mSessionId); + } + return null; + } + + public boolean endSession() { + if (mSessionValid) { + mSessionValid = false; + return true; + } + return false; + } + + public MediaSessionStatus getSessionStatus(String sid) { + int sessionState = (sid != null && sid.equals(mSessionId)) ? + MediaSessionStatus.SESSION_STATE_ACTIVE : + MediaSessionStatus.SESSION_STATE_INVALIDATED; + + return new MediaSessionStatus.Builder(sessionState) + .setQueuePaused(mPaused) + .build(); + } + + // Suspend the playback manager. Put the current item back into PENDING + // state, and remember the current playback position. Called when switching + // to a different player (route). + public void suspend(long pos) { + for (PlaylistItem item : mPlaylist) { + item.setRemoteItemId(null); + item.setDuration(0); + } + PlaylistItem item = getCurrentItem(); + if (DEBUG) { + log("suspend: item=" + item + ", pos=" + pos); + } + if (item != null) { + if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PLAYING + || item.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED) { + item.setState(MediaItemStatus.PLAYBACK_STATE_PENDING); + item.setPosition(pos); + } + } + } + + // Unsuspend the playback manager. Restart playback on new player (route). + // This will resume playback of current item. Furthermore, if the new player + // supports queuing, playlist will be re-established on the remote player. + public void unsuspend() { + if (DEBUG) { + log("unsuspend"); + } + if (mPlayer.isQueuingSupported()) { + for (PlaylistItem item : mPlaylist) { + mPlayer.enqueue(item); + } + } + updatePlaybackState(); + } + + // Player.Callback + @Override + public void onError() { + finishItem(true); + } + + @Override + public void onCompletion() { + finishItem(false); + } + + @Override + public void onPlaylistChanged() { + // Playlist has changed, update the cached playlist + updateStatus(); + } + + @Override + public void onPlaylistReady() { + // Notify activity to update Ui + if (mCallback != null) { + mCallback.onStatusChanged(); + } + } + + private void log(String message) { + Log.d(TAG, mName + ": " + message); + } + + private void checkPlayer() { + if (mPlayer == null) { + throw new IllegalStateException("Player not set!"); + } + } + + private void checkSession() { + if (!mSessionValid) { + throw new IllegalStateException("Session not set!"); + } + } + + private void checkPlayerAndSession() { + checkPlayer(); + checkSession(); + } + + private void checkItemCurrent(String iid) { + PlaylistItem item = getCurrentItem(); + if (item == null || !item.getItemId().equals(iid)) { + throw new IllegalArgumentException("Item is not current!"); + } + } + + private void updatePlaybackState() { + PlaylistItem item = getCurrentItem(); + if (item != null) { + if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PENDING) { + item.setState(mPaused ? MediaItemStatus.PLAYBACK_STATE_PAUSED + : MediaItemStatus.PLAYBACK_STATE_PLAYING); + if (!mPlayer.isQueuingSupported()) { + mPlayer.play(item); + } + } else if (mPaused && item.getState() == MediaItemStatus.PLAYBACK_STATE_PLAYING) { + mPlayer.pause(); + item.setState(MediaItemStatus.PLAYBACK_STATE_PAUSED); + } else if (!mPaused && item.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED) { + mPlayer.resume(); + item.setState(MediaItemStatus.PLAYBACK_STATE_PLAYING); + } + // notify client that item playback status has changed + if (mCallback != null) { + mCallback.onItemChanged(item); + } + } + updateStatus(); + } + + private PlaylistItem removeItem(String iid, int state) { + checkPlayerAndSession(); + List<PlaylistItem> queue = + new ArrayList<PlaylistItem>(mPlaylist.size()); + PlaylistItem found = null; + for (PlaylistItem item : mPlaylist) { + if (iid.equals(item.getItemId())) { + if (mPlayer.isQueuingSupported()) { + mPlayer.remove(item.getRemoteItemId()); + } else if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PLAYING + || item.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED){ + mPlayer.stop(); + } + item.setState(state); + found = item; + // notify client that item is now removed + if (mCallback != null) { + mCallback.onItemChanged(found); + } + } else { + queue.add(item); + } + } + if (found != null) { + mPlaylist = queue; + updatePlaybackState(); + } else { + log("item not found"); + } + return found; + } + + private void finishItem(boolean error) { + PlaylistItem item = getCurrentItem(); + if (item != null) { + removeItem(item.getItemId(), error ? + MediaItemStatus.PLAYBACK_STATE_ERROR : + MediaItemStatus.PLAYBACK_STATE_FINISHED); + updateStatus(); + } + } + + // set the Player that this playback manager will interact with + public void setPlayer(Player player) { + mPlayer = player; + checkPlayer(); + mPlayer.setCallback(this); + } + + // provide a callback interface to tell the UI when significant state changes occur + public void setCallback(Callback callback) { + mCallback = callback; + } + + @Override + public String toString() { + String result = "Media Queue: "; + if (!mPlaylist.isEmpty()) { + for (PlaylistItem item : mPlaylist) { + result += "\n" + item.toString(); + } + } else { + result += "<empty>"; + } + return result; + } + + public interface Callback { + void onStatusChanged(); + void onItemChanged(PlaylistItem item); + } +} diff --git a/samples/browseable/MediaRouter/src/com.example.android.mediarouter/provider/SampleMediaRouteProvider.java b/samples/browseable/MediaRouter/src/com.example.android.mediarouter/provider/SampleMediaRouteProvider.java new file mode 100644 index 000000000..739e3ba73 --- /dev/null +++ b/samples/browseable/MediaRouter/src/com.example.android.mediarouter/provider/SampleMediaRouteProvider.java @@ -0,0 +1,602 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.mediarouter.provider; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.IntentFilter.MalformedMimeTypeException; +import android.content.res.Resources; +import android.media.AudioManager; +import android.media.MediaRouter; +import android.net.Uri; +import android.os.Bundle; +import android.support.v7.media.MediaControlIntent; +import android.support.v7.media.MediaRouteDescriptor; +import android.support.v7.media.MediaRouteProvider; +import android.support.v7.media.MediaRouteProviderDescriptor; +import android.support.v7.media.MediaRouter.ControlRequestCallback; +import android.support.v7.media.MediaSessionStatus; +import android.util.Log; + +import com.example.android.mediarouter.player.Player; +import com.example.android.mediarouter.player.PlaylistItem; +import com.example.android.mediarouter.R; +import com.example.android.mediarouter.player.SessionManager; + +import java.util.ArrayList; + +/** + * Demonstrates how to create a custom media route provider. + * + * @see SampleMediaRouteProviderService + */ +public final class SampleMediaRouteProvider extends MediaRouteProvider { + private static final String TAG = "SampleMediaRouteProvider"; + + private static final String FIXED_VOLUME_ROUTE_ID = "fixed"; + private static final String VARIABLE_VOLUME_BASIC_ROUTE_ID = "variable_basic"; + private static final String VARIABLE_VOLUME_QUEUING_ROUTE_ID = "variable_queuing"; + private static final String VARIABLE_VOLUME_SESSION_ROUTE_ID = "variable_session"; + private static final int VOLUME_MAX = 10; + + /** + * A custom media control intent category for special requests that are + * supported by this provider's routes. + */ + public static final String CATEGORY_SAMPLE_ROUTE = + "com.example.android.mediarouteprovider.CATEGORY_SAMPLE_ROUTE"; + + /** + * A custom media control intent action for special requests that are + * supported by this provider's routes. + * <p> + * This particular request is designed to return a bundle of not very + * interesting statistics for demonstration purposes. + * </p> + * + * @see #DATA_PLAYBACK_COUNT + */ + public static final String ACTION_GET_STATISTICS = + "com.example.android.mediarouteprovider.ACTION_GET_STATISTICS"; + + /** + * {@link #ACTION_GET_STATISTICS} result data: Number of times the + * playback action was invoked. + */ + public static final String DATA_PLAYBACK_COUNT = + "com.example.android.mediarouteprovider.EXTRA_PLAYBACK_COUNT"; + + private static final ArrayList<IntentFilter> CONTROL_FILTERS_BASIC; + private static final ArrayList<IntentFilter> CONTROL_FILTERS_QUEUING; + private static final ArrayList<IntentFilter> CONTROL_FILTERS_SESSION; + + static { + IntentFilter f1 = new IntentFilter(); + f1.addCategory(CATEGORY_SAMPLE_ROUTE); + f1.addAction(ACTION_GET_STATISTICS); + + IntentFilter f2 = new IntentFilter(); + f2.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK); + f2.addAction(MediaControlIntent.ACTION_PLAY); + f2.addDataScheme("http"); + f2.addDataScheme("https"); + f2.addDataScheme("rtsp"); + f2.addDataScheme("file"); + addDataTypeUnchecked(f2, "video/*"); + + IntentFilter f3 = new IntentFilter(); + f3.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK); + f3.addAction(MediaControlIntent.ACTION_SEEK); + f3.addAction(MediaControlIntent.ACTION_GET_STATUS); + f3.addAction(MediaControlIntent.ACTION_PAUSE); + f3.addAction(MediaControlIntent.ACTION_RESUME); + f3.addAction(MediaControlIntent.ACTION_STOP); + + IntentFilter f4 = new IntentFilter(); + f4.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK); + f4.addAction(MediaControlIntent.ACTION_ENQUEUE); + f4.addDataScheme("http"); + f4.addDataScheme("https"); + f4.addDataScheme("rtsp"); + f4.addDataScheme("file"); + addDataTypeUnchecked(f4, "video/*"); + + IntentFilter f5 = new IntentFilter(); + f5.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK); + f5.addAction(MediaControlIntent.ACTION_REMOVE); + + IntentFilter f6 = new IntentFilter(); + f6.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK); + f6.addAction(MediaControlIntent.ACTION_START_SESSION); + f6.addAction(MediaControlIntent.ACTION_GET_SESSION_STATUS); + f6.addAction(MediaControlIntent.ACTION_END_SESSION); + + CONTROL_FILTERS_BASIC = new ArrayList<IntentFilter>(); + CONTROL_FILTERS_BASIC.add(f1); + CONTROL_FILTERS_BASIC.add(f2); + CONTROL_FILTERS_BASIC.add(f3); + + CONTROL_FILTERS_QUEUING = + new ArrayList<IntentFilter>(CONTROL_FILTERS_BASIC); + CONTROL_FILTERS_QUEUING.add(f4); + CONTROL_FILTERS_QUEUING.add(f5); + + CONTROL_FILTERS_SESSION = + new ArrayList<IntentFilter>(CONTROL_FILTERS_QUEUING); + CONTROL_FILTERS_SESSION.add(f6); + } + + private static void addDataTypeUnchecked(IntentFilter filter, String type) { + try { + filter.addDataType(type); + } catch (MalformedMimeTypeException ex) { + throw new RuntimeException(ex); + } + } + + private int mVolume = 5; + private int mEnqueueCount; + + public SampleMediaRouteProvider(Context context) { + super(context); + + publishRoutes(); + } + + @Override + public RouteController onCreateRouteController(String routeId) { + return new SampleRouteController(routeId); + } + + private void publishRoutes() { + Resources r = getContext().getResources(); + + MediaRouteDescriptor routeDescriptor1 = new MediaRouteDescriptor.Builder( + FIXED_VOLUME_ROUTE_ID, + r.getString(R.string.fixed_volume_route_name)) + .setDescription(r.getString(R.string.sample_route_description)) + .addControlFilters(CONTROL_FILTERS_BASIC) + .setPlaybackStream(AudioManager.STREAM_MUSIC) + .setPlaybackType(MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE) + .setVolumeHandling(MediaRouter.RouteInfo.PLAYBACK_VOLUME_FIXED) + .setVolume(VOLUME_MAX) + .build(); + + MediaRouteDescriptor routeDescriptor2 = new MediaRouteDescriptor.Builder( + VARIABLE_VOLUME_BASIC_ROUTE_ID, + r.getString(R.string.variable_volume_basic_route_name)) + .setDescription(r.getString(R.string.sample_route_description)) + .addControlFilters(CONTROL_FILTERS_BASIC) + .setPlaybackStream(AudioManager.STREAM_MUSIC) + .setPlaybackType(MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE) + .setVolumeHandling(MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE) + .setVolumeMax(VOLUME_MAX) + .setVolume(mVolume) + .build(); + + MediaRouteDescriptor routeDescriptor3 = new MediaRouteDescriptor.Builder( + VARIABLE_VOLUME_QUEUING_ROUTE_ID, + r.getString(R.string.variable_volume_queuing_route_name)) + .setDescription(r.getString(R.string.sample_route_description)) + .addControlFilters(CONTROL_FILTERS_QUEUING) + .setPlaybackStream(AudioManager.STREAM_MUSIC) + .setPlaybackType(MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE) + .setVolumeHandling(MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE) + .setVolumeMax(VOLUME_MAX) + .setVolume(mVolume) + .build(); + + MediaRouteDescriptor routeDescriptor4 = new MediaRouteDescriptor.Builder( + VARIABLE_VOLUME_SESSION_ROUTE_ID, + r.getString(R.string.variable_volume_session_route_name)) + .setDescription(r.getString(R.string.sample_route_description)) + .addControlFilters(CONTROL_FILTERS_SESSION) + .setPlaybackStream(AudioManager.STREAM_MUSIC) + .setPlaybackType(MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE) + .setVolumeHandling(MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE) + .setVolumeMax(VOLUME_MAX) + .setVolume(mVolume) + .build(); + + MediaRouteProviderDescriptor providerDescriptor = + new MediaRouteProviderDescriptor.Builder() + .addRoute(routeDescriptor1) + .addRoute(routeDescriptor2) + .addRoute(routeDescriptor3) + .addRoute(routeDescriptor4) + .build(); + setDescriptor(providerDescriptor); + } + + private final class SampleRouteController extends MediaRouteProvider.RouteController { + private final String mRouteId; + private final SessionManager mSessionManager = new SessionManager("mrp"); + private final Player mPlayer; + private PendingIntent mSessionReceiver; + + public SampleRouteController(String routeId) { + mRouteId = routeId; + mPlayer = Player.create(getContext(), null); + mSessionManager.setPlayer(mPlayer); + mSessionManager.setCallback(new SessionManager.Callback() { + @Override + public void onStatusChanged() { + } + + @Override + public void onItemChanged(PlaylistItem item) { + handleStatusChange(item); + } + }); + Log.d(TAG, mRouteId + ": Controller created"); + } + + @Override + public void onRelease() { + Log.d(TAG, mRouteId + ": Controller released"); + mPlayer.release(); + } + + @Override + public void onSelect() { + Log.d(TAG, mRouteId + ": Selected"); + mPlayer.connect(null); + } + + @Override + public void onUnselect() { + Log.d(TAG, mRouteId + ": Unselected"); + mPlayer.release(); + } + + @Override + public void onSetVolume(int volume) { + Log.d(TAG, mRouteId + ": Set volume to " + volume); + if (!mRouteId.equals(FIXED_VOLUME_ROUTE_ID)) { + setVolumeInternal(volume); + } + } + + @Override + public void onUpdateVolume(int delta) { + Log.d(TAG, mRouteId + ": Update volume by " + delta); + if (!mRouteId.equals(FIXED_VOLUME_ROUTE_ID)) { + setVolumeInternal(mVolume + delta); + } + } + + @Override + public boolean onControlRequest(Intent intent, ControlRequestCallback callback) { + Log.d(TAG, mRouteId + ": Received control request " + intent); + String action = intent.getAction(); + if (intent.hasCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)) { + boolean success = false; + if (action.equals(MediaControlIntent.ACTION_PLAY)) { + success = handlePlay(intent, callback); + } else if (action.equals(MediaControlIntent.ACTION_ENQUEUE)) { + success = handleEnqueue(intent, callback); + } else if (action.equals(MediaControlIntent.ACTION_REMOVE)) { + success = handleRemove(intent, callback); + } else if (action.equals(MediaControlIntent.ACTION_SEEK)) { + success = handleSeek(intent, callback); + } else if (action.equals(MediaControlIntent.ACTION_GET_STATUS)) { + success = handleGetStatus(intent, callback); + } else if (action.equals(MediaControlIntent.ACTION_PAUSE)) { + success = handlePause(intent, callback); + } else if (action.equals(MediaControlIntent.ACTION_RESUME)) { + success = handleResume(intent, callback); + } else if (action.equals(MediaControlIntent.ACTION_STOP)) { + success = handleStop(intent, callback); + } else if (action.equals(MediaControlIntent.ACTION_START_SESSION)) { + success = handleStartSession(intent, callback); + } else if (action.equals(MediaControlIntent.ACTION_GET_SESSION_STATUS)) { + success = handleGetSessionStatus(intent, callback); + } else if (action.equals(MediaControlIntent.ACTION_END_SESSION)) { + success = handleEndSession(intent, callback); + } + Log.d(TAG, mSessionManager.toString()); + return success; + } + + if (action.equals(ACTION_GET_STATISTICS) + && intent.hasCategory(CATEGORY_SAMPLE_ROUTE)) { + Bundle data = new Bundle(); + data.putInt(DATA_PLAYBACK_COUNT, mEnqueueCount); + if (callback != null) { + callback.onResult(data); + } + return true; + } + return false; + } + + private void setVolumeInternal(int volume) { + if (volume >= 0 && volume <= VOLUME_MAX) { + mVolume = volume; + Log.d(TAG, mRouteId + ": New volume is " + mVolume); + AudioManager audioManager = + (AudioManager)getContext().getSystemService(Context.AUDIO_SERVICE); + audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, volume, 0); + publishRoutes(); + } + } + + private boolean handlePlay(Intent intent, ControlRequestCallback callback) { + String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID); + if (sid != null && !sid.equals(mSessionManager.getSessionId())) { + Log.d(TAG, "handlePlay fails because of bad sid="+sid); + return false; + } + if (mSessionManager.hasSession()) { + mSessionManager.stop(); + } + return handleEnqueue(intent, callback); + } + + private boolean handleEnqueue(Intent intent, ControlRequestCallback callback) { + String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID); + if (sid != null && !sid.equals(mSessionManager.getSessionId())) { + Log.d(TAG, "handleEnqueue fails because of bad sid="+sid); + return false; + } + + Uri uri = intent.getData(); + if (uri == null) { + Log.d(TAG, "handleEnqueue fails because of bad uri="+uri); + return false; + } + + boolean enqueue = intent.getAction().equals(MediaControlIntent.ACTION_ENQUEUE); + String mime = intent.getType(); + long pos = intent.getLongExtra(MediaControlIntent.EXTRA_ITEM_CONTENT_POSITION, 0); + Bundle metadata = intent.getBundleExtra(MediaControlIntent.EXTRA_ITEM_METADATA); + Bundle headers = intent.getBundleExtra(MediaControlIntent.EXTRA_ITEM_HTTP_HEADERS); + PendingIntent receiver = (PendingIntent)intent.getParcelableExtra( + MediaControlIntent.EXTRA_ITEM_STATUS_UPDATE_RECEIVER); + + Log.d(TAG, mRouteId + ": Received " + (enqueue?"enqueue":"play") + " request" + + ", uri=" + uri + + ", mime=" + mime + + ", sid=" + sid + + ", pos=" + pos + + ", metadata=" + metadata + + ", headers=" + headers + + ", receiver=" + receiver); + PlaylistItem item = mSessionManager.add(uri, mime, receiver); + if (callback != null) { + if (item != null) { + Bundle result = new Bundle(); + result.putString(MediaControlIntent.EXTRA_SESSION_ID, item.getSessionId()); + result.putString(MediaControlIntent.EXTRA_ITEM_ID, item.getItemId()); + result.putBundle(MediaControlIntent.EXTRA_ITEM_STATUS, + item.getStatus().asBundle()); + callback.onResult(result); + } else { + callback.onError("Failed to open " + uri.toString(), null); + } + } + mEnqueueCount +=1; + return true; + } + + private boolean handleRemove(Intent intent, ControlRequestCallback callback) { + String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID); + if (sid == null || !sid.equals(mSessionManager.getSessionId())) { + return false; + } + + String iid = intent.getStringExtra(MediaControlIntent.EXTRA_ITEM_ID); + PlaylistItem item = mSessionManager.remove(iid); + if (callback != null) { + if (item != null) { + Bundle result = new Bundle(); + result.putBundle(MediaControlIntent.EXTRA_ITEM_STATUS, + item.getStatus().asBundle()); + callback.onResult(result); + } else { + callback.onError("Failed to remove" + + ", sid=" + sid + ", iid=" + iid, null); + } + } + return (item != null); + } + + private boolean handleSeek(Intent intent, ControlRequestCallback callback) { + String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID); + if (sid == null || !sid.equals(mSessionManager.getSessionId())) { + return false; + } + + String iid = intent.getStringExtra(MediaControlIntent.EXTRA_ITEM_ID); + long pos = intent.getLongExtra(MediaControlIntent.EXTRA_ITEM_CONTENT_POSITION, 0); + Log.d(TAG, mRouteId + ": Received seek request, pos=" + pos); + PlaylistItem item = mSessionManager.seek(iid, pos); + if (callback != null) { + if (item != null) { + Bundle result = new Bundle(); + result.putBundle(MediaControlIntent.EXTRA_ITEM_STATUS, + item.getStatus().asBundle()); + callback.onResult(result); + } else { + callback.onError("Failed to seek" + + ", sid=" + sid + ", iid=" + iid + ", pos=" + pos, null); + } + } + return (item != null); + } + + private boolean handleGetStatus(Intent intent, ControlRequestCallback callback) { + String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID); + String iid = intent.getStringExtra(MediaControlIntent.EXTRA_ITEM_ID); + Log.d(TAG, mRouteId + ": Received getStatus request, sid=" + sid + ", iid=" + iid); + PlaylistItem item = mSessionManager.getStatus(iid); + if (callback != null) { + if (item != null) { + Bundle result = new Bundle(); + result.putBundle(MediaControlIntent.EXTRA_ITEM_STATUS, + item.getStatus().asBundle()); + callback.onResult(result); + } else { + callback.onError("Failed to get status" + + ", sid=" + sid + ", iid=" + iid, null); + } + } + return (item != null); + } + + private boolean handlePause(Intent intent, ControlRequestCallback callback) { + String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID); + boolean success = (sid != null) && sid.equals(mSessionManager.getSessionId()); + mSessionManager.pause(); + if (callback != null) { + if (success) { + callback.onResult(new Bundle()); + handleSessionStatusChange(sid); + } else { + callback.onError("Failed to pause, sid=" + sid, null); + } + } + return success; + } + + private boolean handleResume(Intent intent, ControlRequestCallback callback) { + String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID); + boolean success = (sid != null) && sid.equals(mSessionManager.getSessionId()); + mSessionManager.resume(); + if (callback != null) { + if (success) { + callback.onResult(new Bundle()); + handleSessionStatusChange(sid); + } else { + callback.onError("Failed to resume, sid=" + sid, null); + } + } + return success; + } + + private boolean handleStop(Intent intent, ControlRequestCallback callback) { + String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID); + boolean success = (sid != null) && sid.equals(mSessionManager.getSessionId()); + mSessionManager.stop(); + if (callback != null) { + if (success) { + callback.onResult(new Bundle()); + handleSessionStatusChange(sid); + } else { + callback.onError("Failed to stop, sid=" + sid, null); + } + } + return success; + } + + private boolean handleStartSession(Intent intent, ControlRequestCallback callback) { + String sid = mSessionManager.startSession(); + Log.d(TAG, "StartSession returns sessionId "+sid); + if (callback != null) { + if (sid != null) { + Bundle result = new Bundle(); + result.putString(MediaControlIntent.EXTRA_SESSION_ID, sid); + result.putBundle(MediaControlIntent.EXTRA_SESSION_STATUS, + mSessionManager.getSessionStatus(sid).asBundle()); + callback.onResult(result); + mSessionReceiver = (PendingIntent)intent.getParcelableExtra( + MediaControlIntent.EXTRA_SESSION_STATUS_UPDATE_RECEIVER); + handleSessionStatusChange(sid); + } else { + callback.onError("Failed to start session.", null); + } + } + return (sid != null); + } + + private boolean handleGetSessionStatus(Intent intent, ControlRequestCallback callback) { + String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID); + + MediaSessionStatus sessionStatus = mSessionManager.getSessionStatus(sid); + if (callback != null) { + if (sessionStatus != null) { + Bundle result = new Bundle(); + result.putBundle(MediaControlIntent.EXTRA_SESSION_STATUS, + mSessionManager.getSessionStatus(sid).asBundle()); + callback.onResult(result); + } else { + callback.onError("Failed to get session status, sid=" + sid, null); + } + } + return (sessionStatus != null); + } + + private boolean handleEndSession(Intent intent, ControlRequestCallback callback) { + String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID); + boolean success = (sid != null) && sid.equals(mSessionManager.getSessionId()) + && mSessionManager.endSession(); + if (callback != null) { + if (success) { + Bundle result = new Bundle(); + MediaSessionStatus sessionStatus = new MediaSessionStatus.Builder( + MediaSessionStatus.SESSION_STATE_ENDED).build(); + result.putBundle(MediaControlIntent.EXTRA_SESSION_STATUS, sessionStatus.asBundle()); + callback.onResult(result); + handleSessionStatusChange(sid); + mSessionReceiver = null; + } else { + callback.onError("Failed to end session, sid=" + sid, null); + } + } + return success; + } + + private void handleStatusChange(PlaylistItem item) { + if (item == null) { + item = mSessionManager.getCurrentItem(); + } + if (item != null) { + PendingIntent receiver = item.getUpdateReceiver(); + if (receiver != null) { + Intent intent = new Intent(); + intent.putExtra(MediaControlIntent.EXTRA_SESSION_ID, item.getSessionId()); + intent.putExtra(MediaControlIntent.EXTRA_ITEM_ID, item.getItemId()); + intent.putExtra(MediaControlIntent.EXTRA_ITEM_STATUS, + item.getStatus().asBundle()); + try { + receiver.send(getContext(), 0, intent); + Log.d(TAG, mRouteId + ": Sending status update from provider"); + } catch (PendingIntent.CanceledException e) { + Log.d(TAG, mRouteId + ": Failed to send status update!"); + } + } + } + } + + private void handleSessionStatusChange(String sid) { + if (mSessionReceiver != null) { + Intent intent = new Intent(); + intent.putExtra(MediaControlIntent.EXTRA_SESSION_ID, sid); + intent.putExtra(MediaControlIntent.EXTRA_SESSION_STATUS, + mSessionManager.getSessionStatus(sid).asBundle()); + try { + mSessionReceiver.send(getContext(), 0, intent); + Log.d(TAG, mRouteId + ": Sending session status update from provider"); + } catch (PendingIntent.CanceledException e) { + Log.d(TAG, mRouteId + ": Failed to send session status update!"); + } + } + } + } +} diff --git a/samples/browseable/MediaRouter/src/com.example.android.mediarouter/provider/SampleMediaRouteProviderService.java b/samples/browseable/MediaRouter/src/com.example.android.mediarouter/provider/SampleMediaRouteProviderService.java new file mode 100644 index 000000000..41a6cbdf4 --- /dev/null +++ b/samples/browseable/MediaRouter/src/com.example.android.mediarouter/provider/SampleMediaRouteProviderService.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.mediarouter.provider; + +import android.support.v7.media.MediaRouteProvider; +import android.support.v7.media.MediaRouteProviderService; + +import com.example.android.mediarouter.provider.SampleMediaRouteProvider; + +/** + * Demonstrates how to register a custom media route provider service + * using the support library. + * + * @see com.example.android.mediarouter.provider.SampleMediaRouteProvider + */ +public class SampleMediaRouteProviderService extends MediaRouteProviderService { + @Override + public MediaRouteProvider onCreateMediaRouteProvider() { + return new SampleMediaRouteProvider(this); + } +} |