summaryrefslogtreecommitdiffstats
path: root/samples/browseable/MediaRouter/src
diff options
context:
space:
mode:
Diffstat (limited to 'samples/browseable/MediaRouter/src')
-rw-r--r--samples/browseable/MediaRouter/src/com.example.android.common.logger/Log.java236
-rw-r--r--samples/browseable/MediaRouter/src/com.example.android.common.logger/LogFragment.java109
-rw-r--r--samples/browseable/MediaRouter/src/com.example.android.common.logger/LogNode.java39
-rw-r--r--samples/browseable/MediaRouter/src/com.example.android.common.logger/LogView.java145
-rw-r--r--samples/browseable/MediaRouter/src/com.example.android.common.logger/LogWrapper.java75
-rw-r--r--samples/browseable/MediaRouter/src/com.example.android.common.logger/MessageOnlyLogFilter.java60
-rw-r--r--samples/browseable/MediaRouter/src/com.example.android.mediarouter/player/LocalPlayer.java630
-rw-r--r--samples/browseable/MediaRouter/src/com.example.android.mediarouter/player/MainActivity.java724
-rw-r--r--samples/browseable/MediaRouter/src/com.example.android.mediarouter/player/OverlayDisplayWindow.java463
-rw-r--r--samples/browseable/MediaRouter/src/com.example.android.mediarouter/player/Player.java81
-rw-r--r--samples/browseable/MediaRouter/src/com.example.android.mediarouter/player/PlaylistItem.java130
-rw-r--r--samples/browseable/MediaRouter/src/com.example.android.mediarouter/player/RemotePlayer.java482
-rw-r--r--samples/browseable/MediaRouter/src/com.example.android.mediarouter/player/SampleMediaButtonReceiver.java46
-rw-r--r--samples/browseable/MediaRouter/src/com.example.android.mediarouter/player/SessionManager.java419
-rw-r--r--samples/browseable/MediaRouter/src/com.example.android.mediarouter/provider/SampleMediaRouteProvider.java602
-rw-r--r--samples/browseable/MediaRouter/src/com.example.android.mediarouter/provider/SampleMediaRouteProviderService.java35
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);
+ }
+}