diff --git a/samples/browseable/ActionBarCompat-Basic/AndroidManifest.xml b/samples/browseable/ActionBarCompat-Basic/AndroidManifest.xml index 332c055be..2e19220d2 100644 --- a/samples/browseable/ActionBarCompat-Basic/AndroidManifest.xml +++ b/samples/browseable/ActionBarCompat-Basic/AndroidManifest.xml @@ -21,9 +21,7 @@ android:versionName="1.0"> - + This sample demonstrates how to create a basic action bar that displays -action items. The sample shows how to inflate items from a menu resource, and -how to add items programatically. To reduce clutter, rarely used actions are -displayed in an action bar overflow.

-

The activity in this sample extends from -{@link android.support.v7.app.ActionBarActivity}, which provides the -functionality necessary to display a compatible action bar on devices -running Android 2.1 and higher.

+

+ + This sample shows you how to use ActionBarCompat to create a basic Activity which + displays action items. It covers inflating items from a menu resource, as well as adding + an item in code. Items that are not shown as action items on the Action Bar are + displayed in the action bar overflow. + +

diff --git a/samples/browseable/ActionBarCompat-Basic/res/values-v21/template-styles.xml b/samples/browseable/ActionBarCompat-Basic/res/values-v21/template-styles.xml new file mode 100644 index 000000000..134fcd9d3 --- /dev/null +++ b/samples/browseable/ActionBarCompat-Basic/res/values-v21/template-styles.xml @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/samples/browseable/SlidingTabsBasic/res/values/strings.xml b/samples/browseable/BluetoothChat/res/values-v11/template-styles.xml old mode 100755 new mode 100644 similarity index 84% rename from samples/browseable/SlidingTabsBasic/res/values/strings.xml rename to samples/browseable/BluetoothChat/res/values-v11/template-styles.xml index 7b9d9ec4f..8c1ea66f2 --- a/samples/browseable/SlidingTabsBasic/res/values/strings.xml +++ b/samples/browseable/BluetoothChat/res/values-v11/template-styles.xml @@ -12,8 +12,11 @@ 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. ---> + --> + - Show Log - Hide Log + + + + + + + diff --git a/samples/browseable/BluetoothChat/src/com.example.android.bluetoothchat/BluetoothChatFragment.java b/samples/browseable/BluetoothChat/src/com.example.android.bluetoothchat/BluetoothChatFragment.java new file mode 100644 index 000000000..8ee906246 --- /dev/null +++ b/samples/browseable/BluetoothChat/src/com.example.android.bluetoothchat/BluetoothChatFragment.java @@ -0,0 +1,402 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.bluetoothchat; + +import android.app.ActionBar; +import android.app.Activity; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentActivity; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.Toast; + +import com.example.android.common.logger.Log; + +/** + * This fragment controls Bluetooth to communicate with other devices. + */ +public class BluetoothChatFragment extends Fragment { + + private static final String TAG = "BluetoothChatFragment"; + + // Intent request codes + private static final int REQUEST_CONNECT_DEVICE_SECURE = 1; + private static final int REQUEST_CONNECT_DEVICE_INSECURE = 2; + private static final int REQUEST_ENABLE_BT = 3; + + // Layout Views + private ListView mConversationView; + private EditText mOutEditText; + private Button mSendButton; + + /** + * Name of the connected device + */ + private String mConnectedDeviceName = null; + + /** + * Array adapter for the conversation thread + */ + private ArrayAdapter mConversationArrayAdapter; + + /** + * String buffer for outgoing messages + */ + private StringBuffer mOutStringBuffer; + + /** + * Local Bluetooth adapter + */ + private BluetoothAdapter mBluetoothAdapter = null; + + /** + * Member object for the chat services + */ + private BluetoothChatService mChatService = null; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + // Get local Bluetooth adapter + mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + + // If the adapter is null, then Bluetooth is not supported + if (mBluetoothAdapter == null) { + FragmentActivity activity = getActivity(); + Toast.makeText(activity, "Bluetooth is not available", Toast.LENGTH_LONG).show(); + activity.finish(); + } + } + + + @Override + public void onStart() { + super.onStart(); + // If BT is not on, request that it be enabled. + // setupChat() will then be called during onActivityResult + if (!mBluetoothAdapter.isEnabled()) { + Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); + startActivityForResult(enableIntent, REQUEST_ENABLE_BT); + // Otherwise, setup the chat session + } else if (mChatService == null) { + setupChat(); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (mChatService != null) { + mChatService.stop(); + } + } + + @Override + public void onResume() { + super.onResume(); + + // Performing this check in onResume() covers the case in which BT was + // not enabled during onStart(), so we were paused to enable it... + // onResume() will be called when ACTION_REQUEST_ENABLE activity returns. + if (mChatService != null) { + // Only if the state is STATE_NONE, do we know that we haven't started already + if (mChatService.getState() == BluetoothChatService.STATE_NONE) { + // Start the Bluetooth chat services + mChatService.start(); + } + } + } + + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_bluetooth_chat, container, false); + } + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + mConversationView = (ListView) view.findViewById(R.id.in); + mOutEditText = (EditText) view.findViewById(R.id.edit_text_out); + mSendButton = (Button) view.findViewById(R.id.button_send); + } + + /** + * Set up the UI and background operations for chat. + */ + private void setupChat() { + Log.d(TAG, "setupChat()"); + + // Initialize the array adapter for the conversation thread + mConversationArrayAdapter = new ArrayAdapter(getActivity(), R.layout.message); + + mConversationView.setAdapter(mConversationArrayAdapter); + + // Initialize the compose field with a listener for the return key + mOutEditText.setOnEditorActionListener(mWriteListener); + + // Initialize the send button with a listener that for click events + mSendButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + // Send a message using content of the edit text widget + View view = getView(); + if (null != view) { + TextView textView = (TextView) view.findViewById(R.id.edit_text_out); + String message = textView.getText().toString(); + sendMessage(message); + } + } + }); + + // Initialize the BluetoothChatService to perform bluetooth connections + mChatService = new BluetoothChatService(getActivity(), mHandler); + + // Initialize the buffer for outgoing messages + mOutStringBuffer = new StringBuffer(""); + } + + /** + * Makes this device discoverable. + */ + private void ensureDiscoverable() { + if (mBluetoothAdapter.getScanMode() != + BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) { + Intent discoverableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE); + discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300); + startActivity(discoverableIntent); + } + } + + /** + * Sends a message. + * + * @param message A string of text to send. + */ + private void sendMessage(String message) { + // Check that we're actually connected before trying anything + if (mChatService.getState() != BluetoothChatService.STATE_CONNECTED) { + Toast.makeText(getActivity(), R.string.not_connected, Toast.LENGTH_SHORT).show(); + return; + } + + // Check that there's actually something to send + if (message.length() > 0) { + // Get the message bytes and tell the BluetoothChatService to write + byte[] send = message.getBytes(); + mChatService.write(send); + + // Reset out string buffer to zero and clear the edit text field + mOutStringBuffer.setLength(0); + mOutEditText.setText(mOutStringBuffer); + } + } + + /** + * The action listener for the EditText widget, to listen for the return key + */ + private TextView.OnEditorActionListener mWriteListener + = new TextView.OnEditorActionListener() { + public boolean onEditorAction(TextView view, int actionId, KeyEvent event) { + // If the action is a key-up event on the return key, send the message + if (actionId == EditorInfo.IME_NULL && event.getAction() == KeyEvent.ACTION_UP) { + String message = view.getText().toString(); + sendMessage(message); + } + return true; + } + }; + + /** + * Updates the status on the action bar. + * + * @param resId a string resource ID + */ + private void setStatus(int resId) { + FragmentActivity activity = getActivity(); + if (null == activity) { + return; + } + final ActionBar actionBar = activity.getActionBar(); + if (null == actionBar) { + return; + } + actionBar.setSubtitle(resId); + } + + /** + * Updates the status on the action bar. + * + * @param subTitle status + */ + private void setStatus(CharSequence subTitle) { + FragmentActivity activity = getActivity(); + if (null == activity) { + return; + } + final ActionBar actionBar = activity.getActionBar(); + if (null == actionBar) { + return; + } + actionBar.setSubtitle(subTitle); + } + + /** + * The Handler that gets information back from the BluetoothChatService + */ + private final Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + FragmentActivity activity = getActivity(); + switch (msg.what) { + case Constants.MESSAGE_STATE_CHANGE: + switch (msg.arg1) { + case BluetoothChatService.STATE_CONNECTED: + setStatus(getString(R.string.title_connected_to, mConnectedDeviceName)); + mConversationArrayAdapter.clear(); + break; + case BluetoothChatService.STATE_CONNECTING: + setStatus(R.string.title_connecting); + break; + case BluetoothChatService.STATE_LISTEN: + case BluetoothChatService.STATE_NONE: + setStatus(R.string.title_not_connected); + break; + } + break; + case Constants.MESSAGE_WRITE: + byte[] writeBuf = (byte[]) msg.obj; + // construct a string from the buffer + String writeMessage = new String(writeBuf); + mConversationArrayAdapter.add("Me: " + writeMessage); + break; + case Constants.MESSAGE_READ: + byte[] readBuf = (byte[]) msg.obj; + // construct a string from the valid bytes in the buffer + String readMessage = new String(readBuf, 0, msg.arg1); + mConversationArrayAdapter.add(mConnectedDeviceName + ": " + readMessage); + break; + case Constants.MESSAGE_DEVICE_NAME: + // save the connected device's name + mConnectedDeviceName = msg.getData().getString(Constants.DEVICE_NAME); + if (null != activity) { + Toast.makeText(activity, "Connected to " + + mConnectedDeviceName, Toast.LENGTH_SHORT).show(); + } + break; + case Constants.MESSAGE_TOAST: + if (null != activity) { + Toast.makeText(activity, msg.getData().getString(Constants.TOAST), + Toast.LENGTH_SHORT).show(); + } + break; + } + } + }; + + public void onActivityResult(int requestCode, int resultCode, Intent data) { + switch (requestCode) { + case REQUEST_CONNECT_DEVICE_SECURE: + // When DeviceListActivity returns with a device to connect + if (resultCode == Activity.RESULT_OK) { + connectDevice(data, true); + } + break; + case REQUEST_CONNECT_DEVICE_INSECURE: + // When DeviceListActivity returns with a device to connect + if (resultCode == Activity.RESULT_OK) { + connectDevice(data, false); + } + break; + case REQUEST_ENABLE_BT: + // When the request to enable Bluetooth returns + if (resultCode == Activity.RESULT_OK) { + // Bluetooth is now enabled, so set up a chat session + setupChat(); + } else { + // User did not enable Bluetooth or an error occurred + Log.d(TAG, "BT not enabled"); + Toast.makeText(getActivity(), R.string.bt_not_enabled_leaving, + Toast.LENGTH_SHORT).show(); + getActivity().finish(); + } + } + } + + /** + * Establish connection with other divice + * + * @param data An {@link Intent} with {@link DeviceListActivity#EXTRA_DEVICE_ADDRESS} extra. + * @param secure Socket Security type - Secure (true) , Insecure (false) + */ + private void connectDevice(Intent data, boolean secure) { + // Get the device MAC address + String address = data.getExtras() + .getString(DeviceListActivity.EXTRA_DEVICE_ADDRESS); + // Get the BluetoothDevice object + BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address); + // Attempt to connect to the device + mChatService.connect(device, secure); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.bluetooth_chat, menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.secure_connect_scan: { + // Launch the DeviceListActivity to see devices and do scan + Intent serverIntent = new Intent(getActivity(), DeviceListActivity.class); + startActivityForResult(serverIntent, REQUEST_CONNECT_DEVICE_SECURE); + return true; + } + case R.id.insecure_connect_scan: { + // Launch the DeviceListActivity to see devices and do scan + Intent serverIntent = new Intent(getActivity(), DeviceListActivity.class); + startActivityForResult(serverIntent, REQUEST_CONNECT_DEVICE_INSECURE); + return true; + } + case R.id.discoverable: { + // Ensure this device is discoverable by others + ensureDiscoverable(); + return true; + } + } + return false; + } + +} diff --git a/samples/browseable/BluetoothChat/src/com.example.android.bluetoothchat/BluetoothChatService.java b/samples/browseable/BluetoothChat/src/com.example.android.bluetoothchat/BluetoothChatService.java new file mode 100644 index 000000000..b88b160d2 --- /dev/null +++ b/samples/browseable/BluetoothChat/src/com.example.android.bluetoothchat/BluetoothChatService.java @@ -0,0 +1,519 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.bluetoothchat; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothServerSocket; +import android.bluetooth.BluetoothSocket; +import android.content.Context; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; + +import com.example.android.common.logger.Log; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.UUID; + +/** + * This class does all the work for setting up and managing Bluetooth + * connections with other devices. It has a thread that listens for + * incoming connections, a thread for connecting with a device, and a + * thread for performing data transmissions when connected. + */ +public class BluetoothChatService { + // Debugging + private static final String TAG = "BluetoothChatService"; + + // Name for the SDP record when creating server socket + private static final String NAME_SECURE = "BluetoothChatSecure"; + private static final String NAME_INSECURE = "BluetoothChatInsecure"; + + // Unique UUID for this application + private static final UUID MY_UUID_SECURE = + UUID.fromString("fa87c0d0-afac-11de-8a39-0800200c9a66"); + private static final UUID MY_UUID_INSECURE = + UUID.fromString("8ce255c0-200a-11e0-ac64-0800200c9a66"); + + // Member fields + private final BluetoothAdapter mAdapter; + private final Handler mHandler; + private AcceptThread mSecureAcceptThread; + private AcceptThread mInsecureAcceptThread; + private ConnectThread mConnectThread; + private ConnectedThread mConnectedThread; + private int mState; + + // Constants that indicate the current connection state + public static final int STATE_NONE = 0; // we're doing nothing + public static final int STATE_LISTEN = 1; // now listening for incoming connections + public static final int STATE_CONNECTING = 2; // now initiating an outgoing connection + public static final int STATE_CONNECTED = 3; // now connected to a remote device + + /** + * Constructor. Prepares a new BluetoothChat session. + * + * @param context The UI Activity Context + * @param handler A Handler to send messages back to the UI Activity + */ + public BluetoothChatService(Context context, Handler handler) { + mAdapter = BluetoothAdapter.getDefaultAdapter(); + mState = STATE_NONE; + mHandler = handler; + } + + /** + * Set the current state of the chat connection + * + * @param state An integer defining the current connection state + */ + private synchronized void setState(int state) { + Log.d(TAG, "setState() " + mState + " -> " + state); + mState = state; + + // Give the new state to the Handler so the UI Activity can update + mHandler.obtainMessage(Constants.MESSAGE_STATE_CHANGE, state, -1).sendToTarget(); + } + + /** + * Return the current connection state. + */ + public synchronized int getState() { + return mState; + } + + /** + * Start the chat service. Specifically start AcceptThread to begin a + * session in listening (server) mode. Called by the Activity onResume() + */ + public synchronized void start() { + Log.d(TAG, "start"); + + // Cancel any thread attempting to make a connection + if (mConnectThread != null) { + mConnectThread.cancel(); + mConnectThread = null; + } + + // Cancel any thread currently running a connection + if (mConnectedThread != null) { + mConnectedThread.cancel(); + mConnectedThread = null; + } + + setState(STATE_LISTEN); + + // Start the thread to listen on a BluetoothServerSocket + if (mSecureAcceptThread == null) { + mSecureAcceptThread = new AcceptThread(true); + mSecureAcceptThread.start(); + } + if (mInsecureAcceptThread == null) { + mInsecureAcceptThread = new AcceptThread(false); + mInsecureAcceptThread.start(); + } + } + + /** + * Start the ConnectThread to initiate a connection to a remote device. + * + * @param device The BluetoothDevice to connect + * @param secure Socket Security type - Secure (true) , Insecure (false) + */ + public synchronized void connect(BluetoothDevice device, boolean secure) { + Log.d(TAG, "connect to: " + device); + + // Cancel any thread attempting to make a connection + if (mState == STATE_CONNECTING) { + if (mConnectThread != null) { + mConnectThread.cancel(); + mConnectThread = null; + } + } + + // Cancel any thread currently running a connection + if (mConnectedThread != null) { + mConnectedThread.cancel(); + mConnectedThread = null; + } + + // Start the thread to connect with the given device + mConnectThread = new ConnectThread(device, secure); + mConnectThread.start(); + setState(STATE_CONNECTING); + } + + /** + * Start the ConnectedThread to begin managing a Bluetooth connection + * + * @param socket The BluetoothSocket on which the connection was made + * @param device The BluetoothDevice that has been connected + */ + public synchronized void connected(BluetoothSocket socket, BluetoothDevice + device, final String socketType) { + Log.d(TAG, "connected, Socket Type:" + socketType); + + // Cancel the thread that completed the connection + if (mConnectThread != null) { + mConnectThread.cancel(); + mConnectThread = null; + } + + // Cancel any thread currently running a connection + if (mConnectedThread != null) { + mConnectedThread.cancel(); + mConnectedThread = null; + } + + // Cancel the accept thread because we only want to connect to one device + if (mSecureAcceptThread != null) { + mSecureAcceptThread.cancel(); + mSecureAcceptThread = null; + } + if (mInsecureAcceptThread != null) { + mInsecureAcceptThread.cancel(); + mInsecureAcceptThread = null; + } + + // Start the thread to manage the connection and perform transmissions + mConnectedThread = new ConnectedThread(socket, socketType); + mConnectedThread.start(); + + // Send the name of the connected device back to the UI Activity + Message msg = mHandler.obtainMessage(Constants.MESSAGE_DEVICE_NAME); + Bundle bundle = new Bundle(); + bundle.putString(Constants.DEVICE_NAME, device.getName()); + msg.setData(bundle); + mHandler.sendMessage(msg); + + setState(STATE_CONNECTED); + } + + /** + * Stop all threads + */ + public synchronized void stop() { + Log.d(TAG, "stop"); + + if (mConnectThread != null) { + mConnectThread.cancel(); + mConnectThread = null; + } + + if (mConnectedThread != null) { + mConnectedThread.cancel(); + mConnectedThread = null; + } + + if (mSecureAcceptThread != null) { + mSecureAcceptThread.cancel(); + mSecureAcceptThread = null; + } + + if (mInsecureAcceptThread != null) { + mInsecureAcceptThread.cancel(); + mInsecureAcceptThread = null; + } + setState(STATE_NONE); + } + + /** + * Write to the ConnectedThread in an unsynchronized manner + * + * @param out The bytes to write + * @see ConnectedThread#write(byte[]) + */ + public void write(byte[] out) { + // Create temporary object + ConnectedThread r; + // Synchronize a copy of the ConnectedThread + synchronized (this) { + if (mState != STATE_CONNECTED) return; + r = mConnectedThread; + } + // Perform the write unsynchronized + r.write(out); + } + + /** + * Indicate that the connection attempt failed and notify the UI Activity. + */ + private void connectionFailed() { + // Send a failure message back to the Activity + Message msg = mHandler.obtainMessage(Constants.MESSAGE_TOAST); + Bundle bundle = new Bundle(); + bundle.putString(Constants.TOAST, "Unable to connect device"); + msg.setData(bundle); + mHandler.sendMessage(msg); + + // Start the service over to restart listening mode + BluetoothChatService.this.start(); + } + + /** + * Indicate that the connection was lost and notify the UI Activity. + */ + private void connectionLost() { + // Send a failure message back to the Activity + Message msg = mHandler.obtainMessage(Constants.MESSAGE_TOAST); + Bundle bundle = new Bundle(); + bundle.putString(Constants.TOAST, "Device connection was lost"); + msg.setData(bundle); + mHandler.sendMessage(msg); + + // Start the service over to restart listening mode + BluetoothChatService.this.start(); + } + + /** + * This thread runs while listening for incoming connections. It behaves + * like a server-side client. It runs until a connection is accepted + * (or until cancelled). + */ + private class AcceptThread extends Thread { + // The local server socket + private final BluetoothServerSocket mmServerSocket; + private String mSocketType; + + public AcceptThread(boolean secure) { + BluetoothServerSocket tmp = null; + mSocketType = secure ? "Secure" : "Insecure"; + + // Create a new listening server socket + try { + if (secure) { + tmp = mAdapter.listenUsingRfcommWithServiceRecord(NAME_SECURE, + MY_UUID_SECURE); + } else { + tmp = mAdapter.listenUsingInsecureRfcommWithServiceRecord( + NAME_INSECURE, MY_UUID_INSECURE); + } + } catch (IOException e) { + Log.e(TAG, "Socket Type: " + mSocketType + "listen() failed", e); + } + mmServerSocket = tmp; + } + + public void run() { + Log.d(TAG, "Socket Type: " + mSocketType + + "BEGIN mAcceptThread" + this); + setName("AcceptThread" + mSocketType); + + BluetoothSocket socket = null; + + // Listen to the server socket if we're not connected + while (mState != STATE_CONNECTED) { + try { + // This is a blocking call and will only return on a + // successful connection or an exception + socket = mmServerSocket.accept(); + } catch (IOException e) { + Log.e(TAG, "Socket Type: " + mSocketType + "accept() failed", e); + break; + } + + // If a connection was accepted + if (socket != null) { + synchronized (BluetoothChatService.this) { + switch (mState) { + case STATE_LISTEN: + case STATE_CONNECTING: + // Situation normal. Start the connected thread. + connected(socket, socket.getRemoteDevice(), + mSocketType); + break; + case STATE_NONE: + case STATE_CONNECTED: + // Either not ready or already connected. Terminate new socket. + try { + socket.close(); + } catch (IOException e) { + Log.e(TAG, "Could not close unwanted socket", e); + } + break; + } + } + } + } + Log.i(TAG, "END mAcceptThread, socket Type: " + mSocketType); + + } + + public void cancel() { + Log.d(TAG, "Socket Type" + mSocketType + "cancel " + this); + try { + mmServerSocket.close(); + } catch (IOException e) { + Log.e(TAG, "Socket Type" + mSocketType + "close() of server failed", e); + } + } + } + + + /** + * This thread runs while attempting to make an outgoing connection + * with a device. It runs straight through; the connection either + * succeeds or fails. + */ + private class ConnectThread extends Thread { + private final BluetoothSocket mmSocket; + private final BluetoothDevice mmDevice; + private String mSocketType; + + public ConnectThread(BluetoothDevice device, boolean secure) { + mmDevice = device; + BluetoothSocket tmp = null; + mSocketType = secure ? "Secure" : "Insecure"; + + // Get a BluetoothSocket for a connection with the + // given BluetoothDevice + try { + if (secure) { + tmp = device.createRfcommSocketToServiceRecord( + MY_UUID_SECURE); + } else { + tmp = device.createInsecureRfcommSocketToServiceRecord( + MY_UUID_INSECURE); + } + } catch (IOException e) { + Log.e(TAG, "Socket Type: " + mSocketType + "create() failed", e); + } + mmSocket = tmp; + } + + public void run() { + Log.i(TAG, "BEGIN mConnectThread SocketType:" + mSocketType); + setName("ConnectThread" + mSocketType); + + // Always cancel discovery because it will slow down a connection + mAdapter.cancelDiscovery(); + + // Make a connection to the BluetoothSocket + try { + // This is a blocking call and will only return on a + // successful connection or an exception + mmSocket.connect(); + } catch (IOException e) { + // Close the socket + try { + mmSocket.close(); + } catch (IOException e2) { + Log.e(TAG, "unable to close() " + mSocketType + + " socket during connection failure", e2); + } + connectionFailed(); + return; + } + + // Reset the ConnectThread because we're done + synchronized (BluetoothChatService.this) { + mConnectThread = null; + } + + // Start the connected thread + connected(mmSocket, mmDevice, mSocketType); + } + + public void cancel() { + try { + mmSocket.close(); + } catch (IOException e) { + Log.e(TAG, "close() of connect " + mSocketType + " socket failed", e); + } + } + } + + /** + * This thread runs during a connection with a remote device. + * It handles all incoming and outgoing transmissions. + */ + private class ConnectedThread extends Thread { + private final BluetoothSocket mmSocket; + private final InputStream mmInStream; + private final OutputStream mmOutStream; + + public ConnectedThread(BluetoothSocket socket, String socketType) { + Log.d(TAG, "create ConnectedThread: " + socketType); + mmSocket = socket; + InputStream tmpIn = null; + OutputStream tmpOut = null; + + // Get the BluetoothSocket input and output streams + try { + tmpIn = socket.getInputStream(); + tmpOut = socket.getOutputStream(); + } catch (IOException e) { + Log.e(TAG, "temp sockets not created", e); + } + + mmInStream = tmpIn; + mmOutStream = tmpOut; + } + + public void run() { + Log.i(TAG, "BEGIN mConnectedThread"); + byte[] buffer = new byte[1024]; + int bytes; + + // Keep listening to the InputStream while connected + while (true) { + try { + // Read from the InputStream + bytes = mmInStream.read(buffer); + + // Send the obtained bytes to the UI Activity + mHandler.obtainMessage(Constants.MESSAGE_READ, bytes, -1, buffer) + .sendToTarget(); + } catch (IOException e) { + Log.e(TAG, "disconnected", e); + connectionLost(); + // Start the service over to restart listening mode + BluetoothChatService.this.start(); + break; + } + } + } + + /** + * Write to the connected OutStream. + * + * @param buffer The bytes to write + */ + public void write(byte[] buffer) { + try { + mmOutStream.write(buffer); + + // Share the sent message back to the UI Activity + mHandler.obtainMessage(Constants.MESSAGE_WRITE, -1, -1, buffer) + .sendToTarget(); + } catch (IOException e) { + Log.e(TAG, "Exception during write", e); + } + } + + public void cancel() { + try { + mmSocket.close(); + } catch (IOException e) { + Log.e(TAG, "close() of connect socket failed", e); + } + } + } +} diff --git a/samples/browseable/BluetoothChat/src/com.example.android.bluetoothchat/Constants.java b/samples/browseable/BluetoothChat/src/com.example.android.bluetoothchat/Constants.java new file mode 100644 index 000000000..3500e8e70 --- /dev/null +++ b/samples/browseable/BluetoothChat/src/com.example.android.bluetoothchat/Constants.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.bluetoothchat; + +/** + * Defines several constants used between {@link BluetoothChatService} and the UI. + */ +public interface Constants { + + // Message types sent from the BluetoothChatService Handler + public static final int MESSAGE_STATE_CHANGE = 1; + public static final int MESSAGE_READ = 2; + public static final int MESSAGE_WRITE = 3; + public static final int MESSAGE_DEVICE_NAME = 4; + public static final int MESSAGE_TOAST = 5; + + // Key names received from the BluetoothChatService Handler + public static final String DEVICE_NAME = "device_name"; + public static final String TOAST = "toast"; + +} diff --git a/samples/browseable/BluetoothChat/src/com.example.android.bluetoothchat/DeviceListActivity.java b/samples/browseable/BluetoothChat/src/com.example.android.bluetoothchat/DeviceListActivity.java new file mode 100644 index 000000000..8b70adc4c --- /dev/null +++ b/samples/browseable/BluetoothChat/src/com.example.android.bluetoothchat/DeviceListActivity.java @@ -0,0 +1,216 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.bluetoothchat; + +import android.app.Activity; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Bundle; +import android.view.View; +import android.view.Window; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.ListView; +import android.widget.TextView; + +import com.example.android.common.logger.Log; + +import java.util.Set; + +/** + * This Activity appears as a dialog. It lists any paired devices and + * devices detected in the area after discovery. When a device is chosen + * by the user, the MAC address of the device is sent back to the parent + * Activity in the result Intent. + */ +public class DeviceListActivity extends Activity { + + /** + * Tag for Log + */ + private static final String TAG = "DeviceListActivity"; + + /** + * Return Intent extra + */ + public static String EXTRA_DEVICE_ADDRESS = "device_address"; + + /** + * Member fields + */ + private BluetoothAdapter mBtAdapter; + + /** + * Newly discovered devices + */ + private ArrayAdapter mNewDevicesArrayAdapter; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Setup the window + requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); + setContentView(R.layout.activity_device_list); + + // Set result CANCELED in case the user backs out + setResult(Activity.RESULT_CANCELED); + + // Initialize the button to perform device discovery + Button scanButton = (Button) findViewById(R.id.button_scan); + scanButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + doDiscovery(); + v.setVisibility(View.GONE); + } + }); + + // Initialize array adapters. One for already paired devices and + // one for newly discovered devices + ArrayAdapter pairedDevicesArrayAdapter = + new ArrayAdapter(this, R.layout.device_name); + mNewDevicesArrayAdapter = new ArrayAdapter(this, R.layout.device_name); + + // Find and set up the ListView for paired devices + ListView pairedListView = (ListView) findViewById(R.id.paired_devices); + pairedListView.setAdapter(pairedDevicesArrayAdapter); + pairedListView.setOnItemClickListener(mDeviceClickListener); + + // Find and set up the ListView for newly discovered devices + ListView newDevicesListView = (ListView) findViewById(R.id.new_devices); + newDevicesListView.setAdapter(mNewDevicesArrayAdapter); + newDevicesListView.setOnItemClickListener(mDeviceClickListener); + + // Register for broadcasts when a device is discovered + IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND); + this.registerReceiver(mReceiver, filter); + + // Register for broadcasts when discovery has finished + filter = new IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_FINISHED); + this.registerReceiver(mReceiver, filter); + + // Get the local Bluetooth adapter + mBtAdapter = BluetoothAdapter.getDefaultAdapter(); + + // Get a set of currently paired devices + Set pairedDevices = mBtAdapter.getBondedDevices(); + + // If there are paired devices, add each one to the ArrayAdapter + if (pairedDevices.size() > 0) { + findViewById(R.id.title_paired_devices).setVisibility(View.VISIBLE); + for (BluetoothDevice device : pairedDevices) { + pairedDevicesArrayAdapter.add(device.getName() + "\n" + device.getAddress()); + } + } else { + String noDevices = getResources().getText(R.string.none_paired).toString(); + pairedDevicesArrayAdapter.add(noDevices); + } + } + + @Override + protected void onDestroy() { + super.onDestroy(); + + // Make sure we're not doing discovery anymore + if (mBtAdapter != null) { + mBtAdapter.cancelDiscovery(); + } + + // Unregister broadcast listeners + this.unregisterReceiver(mReceiver); + } + + /** + * Start device discover with the BluetoothAdapter + */ + private void doDiscovery() { + Log.d(TAG, "doDiscovery()"); + + // Indicate scanning in the title + setProgressBarIndeterminateVisibility(true); + setTitle(R.string.scanning); + + // Turn on sub-title for new devices + findViewById(R.id.title_new_devices).setVisibility(View.VISIBLE); + + // If we're already discovering, stop it + if (mBtAdapter.isDiscovering()) { + mBtAdapter.cancelDiscovery(); + } + + // Request discover from BluetoothAdapter + mBtAdapter.startDiscovery(); + } + + /** + * The on-click listener for all devices in the ListViews + */ + private AdapterView.OnItemClickListener mDeviceClickListener + = new AdapterView.OnItemClickListener() { + public void onItemClick(AdapterView av, View v, int arg2, long arg3) { + // Cancel discovery because it's costly and we're about to connect + mBtAdapter.cancelDiscovery(); + + // Get the device MAC address, which is the last 17 chars in the View + String info = ((TextView) v).getText().toString(); + String address = info.substring(info.length() - 17); + + // Create the result Intent and include the MAC address + Intent intent = new Intent(); + intent.putExtra(EXTRA_DEVICE_ADDRESS, address); + + // Set result and finish this Activity + setResult(Activity.RESULT_OK, intent); + finish(); + } + }; + + /** + * The BroadcastReceiver that listens for discovered devices and changes the title when + * discovery is finished + */ + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + + // When discovery finds a device + if (BluetoothDevice.ACTION_FOUND.equals(action)) { + // Get the BluetoothDevice object from the Intent + BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); + // If it's already paired, skip it, because it's been listed already + if (device.getBondState() != BluetoothDevice.BOND_BONDED) { + mNewDevicesArrayAdapter.add(device.getName() + "\n" + device.getAddress()); + } + // When discovery is finished, change the Activity title + } else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) { + setProgressBarIndeterminateVisibility(false); + setTitle(R.string.select_device); + if (mNewDevicesArrayAdapter.getCount() == 0) { + String noDevices = getResources().getText(R.string.none_found).toString(); + mNewDevicesArrayAdapter.add(noDevices); + } + } + } + }; + +} diff --git a/samples/browseable/BluetoothChat/src/com.example.android.bluetoothchat/MainActivity.java b/samples/browseable/BluetoothChat/src/com.example.android.bluetoothchat/MainActivity.java new file mode 100644 index 000000000..cf4ec47e3 --- /dev/null +++ b/samples/browseable/BluetoothChat/src/com.example.android.bluetoothchat/MainActivity.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. +*/ + +package com.example.android.bluetoothchat; + +import android.os.Bundle; +import android.support.v4.app.FragmentTransaction; +import android.view.Menu; +import android.view.MenuItem; +import android.widget.ViewAnimator; + +import com.example.android.common.activities.SampleActivityBase; +import com.example.android.common.logger.Log; +import com.example.android.common.logger.LogFragment; +import com.example.android.common.logger.LogWrapper; +import com.example.android.common.logger.MessageOnlyLogFilter; + +/** + * A simple launcher activity containing a summary sample description, sample log and a custom + * {@link android.support.v4.app.Fragment} which can display a view. + *

+ * For devices with displays with a width of 720dp or greater, the sample log is always visible, + * on other devices it's visibility is controlled by an item on the Action Bar. + */ +public class MainActivity extends SampleActivityBase { + + public static final String TAG = "MainActivity"; + + // Whether the Log Fragment is currently shown + private boolean mLogShown; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + if (savedInstanceState == null) { + FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); + BluetoothChatFragment fragment = new BluetoothChatFragment(); + transaction.replace(R.id.sample_content_fragment, fragment); + transaction.commit(); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.main, menu); + return true; + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + MenuItem logToggle = menu.findItem(R.id.menu_toggle_log); + logToggle.setVisible(findViewById(R.id.sample_output) instanceof ViewAnimator); + logToggle.setTitle(mLogShown ? R.string.sample_hide_log : R.string.sample_show_log); + + return super.onPrepareOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch(item.getItemId()) { + case R.id.menu_toggle_log: + mLogShown = !mLogShown; + ViewAnimator output = (ViewAnimator) findViewById(R.id.sample_output); + if (mLogShown) { + output.setDisplayedChild(1); + } else { + output.setDisplayedChild(0); + } + supportInvalidateOptionsMenu(); + return true; + } + return super.onOptionsItemSelected(item); + } + + /** Create a chain of targets that will receive log data */ + @Override + public void initializeLogging() { + // Wraps Android's native log framework. + LogWrapper logWrapper = new LogWrapper(); + // Using Log, front-end to the logging chain, emulates android.util.log method signatures. + Log.setLogNode(logWrapper); + + // Filter strips out everything except the message text. + MessageOnlyLogFilter msgFilter = new MessageOnlyLogFilter(); + logWrapper.setNext(msgFilter); + + // On screen logging via a fragment with a TextView. + LogFragment logFragment = (LogFragment) getSupportFragmentManager() + .findFragmentById(R.id.log_fragment); + msgFilter.setNext(logFragment.getLogView()); + + Log.i(TAG, "Ready"); + } +} diff --git a/samples/browseable/AgendaData/Application/src/com.example.android.common/activities/SampleActivityBase.java b/samples/browseable/BluetoothChat/src/com.example.android.common/activities/SampleActivityBase.java similarity index 100% rename from samples/browseable/AgendaData/Application/src/com.example.android.common/activities/SampleActivityBase.java rename to samples/browseable/BluetoothChat/src/com.example.android.common/activities/SampleActivityBase.java diff --git a/samples/browseable/AgendaData/Application/src/com.example.android.common/logger/Log.java b/samples/browseable/BluetoothChat/src/com.example.android.common/logger/Log.java similarity index 100% rename from samples/browseable/AgendaData/Application/src/com.example.android.common/logger/Log.java rename to samples/browseable/BluetoothChat/src/com.example.android.common/logger/Log.java diff --git a/samples/browseable/AgendaData/Application/src/com.example.android.common/logger/LogFragment.java b/samples/browseable/BluetoothChat/src/com.example.android.common/logger/LogFragment.java similarity index 100% rename from samples/browseable/AgendaData/Application/src/com.example.android.common/logger/LogFragment.java rename to samples/browseable/BluetoothChat/src/com.example.android.common/logger/LogFragment.java diff --git a/samples/browseable/AgendaData/Application/src/com.example.android.common/logger/LogNode.java b/samples/browseable/BluetoothChat/src/com.example.android.common/logger/LogNode.java similarity index 100% rename from samples/browseable/AgendaData/Application/src/com.example.android.common/logger/LogNode.java rename to samples/browseable/BluetoothChat/src/com.example.android.common/logger/LogNode.java diff --git a/samples/browseable/AgendaData/Application/src/com.example.android.common/logger/LogView.java b/samples/browseable/BluetoothChat/src/com.example.android.common/logger/LogView.java similarity index 100% rename from samples/browseable/AgendaData/Application/src/com.example.android.common/logger/LogView.java rename to samples/browseable/BluetoothChat/src/com.example.android.common/logger/LogView.java diff --git a/samples/browseable/AgendaData/Application/src/com.example.android.common/logger/LogWrapper.java b/samples/browseable/BluetoothChat/src/com.example.android.common/logger/LogWrapper.java similarity index 100% rename from samples/browseable/AgendaData/Application/src/com.example.android.common/logger/LogWrapper.java rename to samples/browseable/BluetoothChat/src/com.example.android.common/logger/LogWrapper.java diff --git a/samples/browseable/AgendaData/Application/src/com.example.android.common/logger/MessageOnlyLogFilter.java b/samples/browseable/BluetoothChat/src/com.example.android.common/logger/MessageOnlyLogFilter.java similarity index 100% rename from samples/browseable/AgendaData/Application/src/com.example.android.common/logger/MessageOnlyLogFilter.java rename to samples/browseable/BluetoothChat/src/com.example.android.common/logger/MessageOnlyLogFilter.java diff --git a/samples/browseable/BluetoothLeGatt/AndroidManifest.xml b/samples/browseable/BluetoothLeGatt/AndroidManifest.xml index babd6df2d..d3cf25757 100644 --- a/samples/browseable/BluetoothLeGatt/AndroidManifest.xml +++ b/samples/browseable/BluetoothLeGatt/AndroidManifest.xml @@ -22,8 +22,8 @@ android:versionCode="1" android:versionName="1.0"> - + + + + + + + - + + diff --git a/samples/browseable/DoneBar/src/com.example.android.donebar/MainActivity.java b/samples/browseable/DoneBar/src/com.example.android.donebar/MainActivity.java index 8b1e8a469..c51996c4c 100644 --- a/samples/browseable/DoneBar/src/com.example.android.donebar/MainActivity.java +++ b/samples/browseable/DoneBar/src/com.example.android.donebar/MainActivity.java @@ -14,9 +14,6 @@ * limitations under the License. */ - - - package com.example.android.donebar; import android.app.Activity; diff --git a/samples/browseable/ElizaChat/Application/AndroidManifest.xml b/samples/browseable/ElizaChat/Application/AndroidManifest.xml index e653fa9d6..14e982383 100644 --- a/samples/browseable/ElizaChat/Application/AndroidManifest.xml +++ b/samples/browseable/ElizaChat/Application/AndroidManifest.xml @@ -15,7 +15,7 @@ --> + package="com.example.android.wearable.elizachat" > @@ -36,8 +36,8 @@ - - + + diff --git a/samples/browseable/ElizaChat/Application/res/values-v21/template-styles.xml b/samples/browseable/ElizaChat/Application/res/values-v21/template-styles.xml new file mode 100644 index 000000000..134fcd9d3 --- /dev/null +++ b/samples/browseable/ElizaChat/Application/res/values-v21/template-styles.xml @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/samples/browseable/MediaBrowserService/res/values/dimens.xml b/samples/browseable/MediaBrowserService/res/values/dimens.xml new file mode 100644 index 000000000..e57a8c919 --- /dev/null +++ b/samples/browseable/MediaBrowserService/res/values/dimens.xml @@ -0,0 +1,21 @@ + + + + 16dp + 4dp + 6dp + diff --git a/samples/browseable/MediaBrowserService/res/values/strings.xml b/samples/browseable/MediaBrowserService/res/values/strings.xml new file mode 100644 index 000000000..7a012e870 --- /dev/null +++ b/samples/browseable/MediaBrowserService/res/values/strings.xml @@ -0,0 +1,33 @@ + + + + + Auto Music Demo + Favorite + Unable to retrieve metadata. + Genres + Songs by genre + %1$s songs + Random music + Cannot skip + Error Loading Media + Play item + Skip to previous + play or pause + Skip to next + + diff --git a/samples/browseable/MediaBrowserService/res/values/strings_notifications.xml b/samples/browseable/MediaBrowserService/res/values/strings_notifications.xml new file mode 100644 index 000000000..f406ba667 --- /dev/null +++ b/samples/browseable/MediaBrowserService/res/values/strings_notifications.xml @@ -0,0 +1,24 @@ + + + + + Pause + Play + Previous + Next + Empty metadata! + diff --git a/samples/browseable/MediaBrowserService/res/values/styles.xml b/samples/browseable/MediaBrowserService/res/values/styles.xml new file mode 100644 index 000000000..3be59c152 --- /dev/null +++ b/samples/browseable/MediaBrowserService/res/values/styles.xml @@ -0,0 +1,26 @@ + + + + + + + + + + \ No newline at end of file diff --git a/samples/browseable/MediaBrowserService/res/xml/automotive_app_desc.xml b/samples/browseable/MediaBrowserService/res/xml/automotive_app_desc.xml new file mode 100644 index 000000000..a84750b04 --- /dev/null +++ b/samples/browseable/MediaBrowserService/res/xml/automotive_app_desc.xml @@ -0,0 +1,19 @@ + + + + + diff --git a/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/BrowseFragment.java b/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/BrowseFragment.java new file mode 100644 index 000000000..726ae15b6 --- /dev/null +++ b/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/BrowseFragment.java @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.mediabrowserservice; + +import android.app.Fragment; +import android.content.ComponentName; +import android.content.Context; +import android.media.browse.MediaBrowser; +import android.media.session.MediaController; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.Toast; + +import com.example.android.mediabrowserservice.utils.LogHelper; + +import java.util.ArrayList; +import java.util.List; + +/** + * A Fragment that lists all the various browsable queues available + * from a {@link android.service.media.MediaBrowserService}. + *

+ * It uses a {@link MediaBrowser} to connect to the {@link MusicService}. Once connected, + * the fragment subscribes to get all the children. All {@link MediaBrowser.MediaItem}'s + * that can be browsed are shown in a ListView. + */ +public class BrowseFragment extends Fragment { + + private static final String TAG = BrowseFragment.class.getSimpleName(); + + public static final String ARG_MEDIA_ID = "media_id"; + + public static interface FragmentDataHelper { + void onMediaItemSelected(MediaBrowser.MediaItem item); + } + + // The mediaId to be used for subscribing for children using the MediaBrowser. + private String mMediaId; + + private MediaBrowser mMediaBrowser; + private BrowseAdapter mBrowserAdapter; + + private MediaBrowser.SubscriptionCallback mSubscriptionCallback = new MediaBrowser.SubscriptionCallback() { + + @Override + public void onChildrenLoaded(String parentId, List children) { + mBrowserAdapter.clear(); + mBrowserAdapter.notifyDataSetInvalidated(); + for (MediaBrowser.MediaItem item : children) { + mBrowserAdapter.add(item); + } + mBrowserAdapter.notifyDataSetChanged(); + } + + @Override + public void onError(String id) { + Toast.makeText(getActivity(), R.string.error_loading_media, + Toast.LENGTH_LONG).show(); + } + }; + + private MediaBrowser.ConnectionCallback mConnectionCallback = + new MediaBrowser.ConnectionCallback() { + @Override + public void onConnected() { + LogHelper.d(TAG, "onConnected: session token " + mMediaBrowser.getSessionToken()); + + if (mMediaId == null) { + mMediaId = mMediaBrowser.getRoot(); + } + mMediaBrowser.subscribe(mMediaId, mSubscriptionCallback); + if (mMediaBrowser.getSessionToken() == null) { + throw new IllegalArgumentException("No Session token"); + } + MediaController mediaController = new MediaController(getActivity(), + mMediaBrowser.getSessionToken()); + getActivity().setMediaController(mediaController); + } + + @Override + public void onConnectionFailed() { + LogHelper.d(TAG, "onConnectionFailed"); + } + + @Override + public void onConnectionSuspended() { + LogHelper.d(TAG, "onConnectionSuspended"); + getActivity().setMediaController(null); + } + }; + + public static BrowseFragment newInstance(String mediaId) { + Bundle args = new Bundle(); + args.putString(ARG_MEDIA_ID, mediaId); + BrowseFragment fragment = new BrowseFragment(); + fragment.setArguments(args); + return fragment; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View rootView = inflater.inflate(R.layout.fragment_list, container, false); + + mBrowserAdapter = new BrowseAdapter(getActivity()); + + View controls = rootView.findViewById(R.id.controls); + controls.setVisibility(View.GONE); + + ListView listView = (ListView) rootView.findViewById(R.id.list_view); + listView.setAdapter(mBrowserAdapter); + listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + MediaBrowser.MediaItem item = mBrowserAdapter.getItem(position); + try { + FragmentDataHelper listener = (FragmentDataHelper) getActivity(); + listener.onMediaItemSelected(item); + } catch (ClassCastException ex) { + Log.e(TAG, "Exception trying to cast to FragmentDataHelper", ex); + } + } + }); + + Bundle args = getArguments(); + mMediaId = args.getString(ARG_MEDIA_ID, null); + + mMediaBrowser = new MediaBrowser(getActivity(), + new ComponentName(getActivity(), MusicService.class), + mConnectionCallback, null); + + return rootView; + } + + @Override + public void onStart() { + super.onStart(); + mMediaBrowser.connect(); + } + + @Override + public void onStop() { + super.onStop(); + mMediaBrowser.disconnect(); + } + + // An adapter for showing the list of browsed MediaItem's + private static class BrowseAdapter extends ArrayAdapter { + + public BrowseAdapter(Context context) { + super(context, R.layout.media_list_item, new ArrayList()); + } + + static class ViewHolder { + ImageView mImageView; + TextView mTitleView; + TextView mDescriptionView; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + + ViewHolder holder; + + if (convertView == null) { + convertView = LayoutInflater.from(getContext()) + .inflate(R.layout.media_list_item, parent, false); + holder = new ViewHolder(); + holder.mImageView = (ImageView) convertView.findViewById(R.id.play_eq); + holder.mImageView.setVisibility(View.GONE); + holder.mTitleView = (TextView) convertView.findViewById(R.id.title); + holder.mDescriptionView = (TextView) convertView.findViewById(R.id.description); + convertView.setTag(holder); + } else { + holder = (ViewHolder) convertView.getTag(); + } + + MediaBrowser.MediaItem item = getItem(position); + holder.mTitleView.setText(item.getDescription().getTitle()); + holder.mDescriptionView.setText(item.getDescription().getDescription()); + if (item.isPlayable()) { + holder.mImageView.setImageDrawable( + getContext().getDrawable(R.drawable.ic_play_arrow_white_24dp)); + holder.mImageView.setVisibility(View.VISIBLE); + } + return convertView; + } + } +} diff --git a/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/MediaNotification.java b/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/MediaNotification.java new file mode 100644 index 000000000..7b8631a45 --- /dev/null +++ b/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/MediaNotification.java @@ -0,0 +1,381 @@ +/* + * Copyright (C) 2014 Google Inc. All Rights Reserved. + * + * 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.mediabrowserservice; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Color; +import android.media.MediaDescription; +import android.media.MediaMetadata; +import android.media.session.MediaController; +import android.media.session.MediaSession; +import android.media.session.PlaybackState; +import android.os.AsyncTask; +import android.util.LruCache; +import android.util.SparseArray; + +import com.example.android.mediabrowserservice.utils.BitmapHelper; +import com.example.android.mediabrowserservice.utils.LogHelper; + +import java.io.IOException; + +/** + * Keeps track of a notification and updates it automatically for a given + * MediaSession. Maintaining a visible notification (usually) guarantees that the music service + * won't be killed during playback. + */ +public class MediaNotification extends BroadcastReceiver { + private static final String TAG = "MediaNotification"; + + private static final int NOTIFICATION_ID = 412; + + public static final String ACTION_PAUSE = "com.example.android.mediabrowserservice.pause"; + public static final String ACTION_PLAY = "com.example.android.mediabrowserservice.play"; + public static final String ACTION_PREV = "com.example.android.mediabrowserservice.prev"; + public static final String ACTION_NEXT = "com.example.android.mediabrowserservice.next"; + + private static final int MAX_ALBUM_ART_CACHE_SIZE = 1024*1024; + + private final MusicService mService; + private MediaSession.Token mSessionToken; + private MediaController mController; + private MediaController.TransportControls mTransportControls; + private final SparseArray mIntents = new SparseArray(); + private final LruCache mAlbumArtCache; + + private PlaybackState mPlaybackState; + private MediaMetadata mMetadata; + + private Notification.Builder mNotificationBuilder; + private NotificationManager mNotificationManager; + private Notification.Action mPlayPauseAction; + + private String mCurrentAlbumArt; + private int mNotificationColor; + + private boolean mStarted = false; + + public MediaNotification(MusicService service) { + mService = service; + updateSessionToken(); + + // simple album art cache that holds no more than + // MAX_ALBUM_ART_CACHE_SIZE bytes: + mAlbumArtCache = new LruCache(MAX_ALBUM_ART_CACHE_SIZE) { + @Override + protected int sizeOf(String key, Bitmap value) { + return value.getByteCount(); + } + }; + + mNotificationColor = getNotificationColor(); + + mNotificationManager = (NotificationManager) mService + .getSystemService(Context.NOTIFICATION_SERVICE); + + String pkg = mService.getPackageName(); + mIntents.put(R.drawable.ic_pause_white_24dp, PendingIntent.getBroadcast(mService, 100, + new Intent(ACTION_PAUSE).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT)); + mIntents.put(R.drawable.ic_play_arrow_white_24dp, PendingIntent.getBroadcast(mService, 100, + new Intent(ACTION_PLAY).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT)); + mIntents.put(R.drawable.ic_skip_previous_white_24dp, PendingIntent.getBroadcast(mService, 100, + new Intent(ACTION_PREV).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT)); + mIntents.put(R.drawable.ic_skip_next_white_24dp, PendingIntent.getBroadcast(mService, 100, + new Intent(ACTION_NEXT).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT)); + } + + protected int getNotificationColor() { + int notificationColor = 0; + String packageName = mService.getPackageName(); + try { + Context packageContext = mService.createPackageContext(packageName, 0); + ApplicationInfo applicationInfo = + mService.getPackageManager().getApplicationInfo(packageName, 0); + packageContext.setTheme(applicationInfo.theme); + Resources.Theme theme = packageContext.getTheme(); + TypedArray ta = theme.obtainStyledAttributes( + new int[] {android.R.attr.colorPrimary}); + notificationColor = ta.getColor(0, Color.DKGRAY); + ta.recycle(); + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + } + return notificationColor; + } + + /** + * Posts the notification and starts tracking the session to keep it + * updated. The notification will automatically be removed if the session is + * destroyed before {@link #stopNotification} is called. + */ + public void startNotification() { + if (!mStarted) { + mController.registerCallback(mCb); + IntentFilter filter = new IntentFilter(); + filter.addAction(ACTION_NEXT); + filter.addAction(ACTION_PAUSE); + filter.addAction(ACTION_PLAY); + filter.addAction(ACTION_PREV); + mService.registerReceiver(this, filter); + + mMetadata = mController.getMetadata(); + mPlaybackState = mController.getPlaybackState(); + + mStarted = true; + // The notification must be updated after setting started to true + updateNotificationMetadata(); + } + } + + /** + * Removes the notification and stops tracking the session. If the session + * was destroyed this has no effect. + */ + public void stopNotification() { + mStarted = false; + mController.unregisterCallback(mCb); + try { + mNotificationManager.cancel(NOTIFICATION_ID); + mService.unregisterReceiver(this); + } catch (IllegalArgumentException ex) { + // ignore if the receiver is not registered. + } + mService.stopForeground(true); + } + + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + LogHelper.d(TAG, "Received intent with action " + action); + if (ACTION_PAUSE.equals(action)) { + mTransportControls.pause(); + } else if (ACTION_PLAY.equals(action)) { + mTransportControls.play(); + } else if (ACTION_NEXT.equals(action)) { + mTransportControls.skipToNext(); + } else if (ACTION_PREV.equals(action)) { + mTransportControls.skipToPrevious(); + } + } + + /** + * Update the state based on a change on the session token. Called either when + * we are running for the first time or when the media session owner has destroyed the session + * (see {@link android.media.session.MediaController.Callback#onSessionDestroyed()}) + */ + private void updateSessionToken() { + MediaSession.Token freshToken = mService.getSessionToken(); + if (mSessionToken == null || !mSessionToken.equals(freshToken)) { + if (mController != null) { + mController.unregisterCallback(mCb); + } + mSessionToken = freshToken; + mController = new MediaController(mService, mSessionToken); + mTransportControls = mController.getTransportControls(); + if (mStarted) { + mController.registerCallback(mCb); + } + } + } + + private final MediaController.Callback mCb = new MediaController.Callback() { + @Override + public void onPlaybackStateChanged(PlaybackState state) { + mPlaybackState = state; + LogHelper.d(TAG, "Received new playback state", state); + updateNotificationPlaybackState(); + } + + @Override + public void onMetadataChanged(MediaMetadata metadata) { + mMetadata = metadata; + LogHelper.d(TAG, "Received new metadata ", metadata); + updateNotificationMetadata(); + } + + @Override + public void onSessionDestroyed() { + super.onSessionDestroyed(); + LogHelper.d(TAG, "Session was destroyed, resetting to the new session token"); + updateSessionToken(); + } + }; + + private void updateNotificationMetadata() { + LogHelper.d(TAG, "updateNotificationMetadata. mMetadata=" + mMetadata); + if (mMetadata == null || mPlaybackState == null) { + return; + } + + updatePlayPauseAction(); + + mNotificationBuilder = new Notification.Builder(mService); + int playPauseActionIndex = 0; + + // If skip to previous action is enabled + if ((mPlaybackState.getActions() & PlaybackState.ACTION_SKIP_TO_PREVIOUS) != 0) { + mNotificationBuilder + .addAction(R.drawable.ic_skip_previous_white_24dp, + mService.getString(R.string.label_previous), + mIntents.get(R.drawable.ic_skip_previous_white_24dp)); + playPauseActionIndex = 1; + } + + mNotificationBuilder.addAction(mPlayPauseAction); + + // If skip to next action is enabled + if ((mPlaybackState.getActions() & PlaybackState.ACTION_SKIP_TO_NEXT) != 0) { + mNotificationBuilder.addAction(R.drawable.ic_skip_next_white_24dp, + mService.getString(R.string.label_next), + mIntents.get(R.drawable.ic_skip_next_white_24dp)); + } + + MediaDescription description = mMetadata.getDescription(); + + String fetchArtUrl = null; + Bitmap art = description.getIconBitmap(); + if (art == null && description.getIconUri() != null) { + // This sample assumes the iconUri will be a valid URL formatted String, but + // it can actually be any valid Android Uri formatted String. + // async fetch the album art icon + String artUrl = description.getIconUri().toString(); + art = mAlbumArtCache.get(artUrl); + if (art == null) { + fetchArtUrl = artUrl; + // use a placeholder art while the remote art is being downloaded + art = BitmapFactory.decodeResource(mService.getResources(), R.drawable.ic_default_art); + } + } + + mNotificationBuilder + .setStyle(new Notification.MediaStyle() + .setShowActionsInCompactView(playPauseActionIndex) // only show play/pause in compact view + .setMediaSession(mSessionToken)) + .setColor(mNotificationColor) + .setSmallIcon(R.drawable.ic_notification) + .setVisibility(Notification.VISIBILITY_PUBLIC) + .setUsesChronometer(true) + .setContentTitle(description.getTitle()) + .setContentText(description.getSubtitle()) + .setLargeIcon(art); + + updateNotificationPlaybackState(); + + mService.startForeground(NOTIFICATION_ID, mNotificationBuilder.build()); + if (fetchArtUrl != null) { + fetchBitmapFromURLAsync(fetchArtUrl); + } + } + + private void updatePlayPauseAction() { + LogHelper.d(TAG, "updatePlayPauseAction"); + String playPauseLabel = ""; + int playPauseIcon; + if (mPlaybackState.getState() == PlaybackState.STATE_PLAYING) { + playPauseLabel = mService.getString(R.string.label_pause); + playPauseIcon = R.drawable.ic_pause_white_24dp; + } else { + playPauseLabel = mService.getString(R.string.label_play); + playPauseIcon = R.drawable.ic_play_arrow_white_24dp; + } + if (mPlayPauseAction == null) { + mPlayPauseAction = new Notification.Action(playPauseIcon, playPauseLabel, + mIntents.get(playPauseIcon)); + } else { + mPlayPauseAction.icon = playPauseIcon; + mPlayPauseAction.title = playPauseLabel; + mPlayPauseAction.actionIntent = mIntents.get(playPauseIcon); + } + } + + private void updateNotificationPlaybackState() { + LogHelper.d(TAG, "updateNotificationPlaybackState. mPlaybackState=" + mPlaybackState); + if (mPlaybackState == null || !mStarted) { + LogHelper.d(TAG, "updateNotificationPlaybackState. cancelling notification!"); + mService.stopForeground(true); + return; + } + if (mNotificationBuilder == null) { + LogHelper.d(TAG, "updateNotificationPlaybackState. there is no notificationBuilder. Ignoring request to update state!"); + return; + } + if (mPlaybackState.getPosition() >= 0) { + LogHelper.d(TAG, "updateNotificationPlaybackState. updating playback position to ", + (System.currentTimeMillis() - mPlaybackState.getPosition()) / 1000, " seconds"); + mNotificationBuilder + .setWhen(System.currentTimeMillis() - mPlaybackState.getPosition()) + .setShowWhen(true) + .setUsesChronometer(true); + mNotificationBuilder.setShowWhen(true); + } else { + LogHelper.d(TAG, "updateNotificationPlaybackState. hiding playback position"); + mNotificationBuilder + .setWhen(0) + .setShowWhen(false) + .setUsesChronometer(false); + } + + updatePlayPauseAction(); + + // Make sure that the notification can be dismissed by the user when we are not playing: + mNotificationBuilder.setOngoing(mPlaybackState.getState() == PlaybackState.STATE_PLAYING); + + mNotificationManager.notify(NOTIFICATION_ID, mNotificationBuilder.build()); + } + + public void fetchBitmapFromURLAsync(final String source) { + LogHelper.d(TAG, "getBitmapFromURLAsync: starting asynctask to fetch ", source); + new AsyncTask() { + @Override + protected Bitmap doInBackground(Void[] objects) { + Bitmap bitmap = null; + try { + bitmap = BitmapHelper.fetchAndRescaleBitmap(source, + BitmapHelper.MEDIA_ART_BIG_WIDTH, BitmapHelper.MEDIA_ART_BIG_HEIGHT); + mAlbumArtCache.put(source, bitmap); + } catch (IOException e) { + LogHelper.e(TAG, e, "getBitmapFromURLAsync: " + source); + } + return bitmap; + } + + @Override + protected void onPostExecute(Bitmap bitmap) { + if (bitmap != null && mMetadata != null && + mNotificationBuilder != null && mMetadata.getDescription() != null && + !source.equals(mMetadata.getDescription().getIconUri())) { + // If the media is still the same, update the notification: + LogHelper.d(TAG, "getBitmapFromURLAsync: set bitmap to ", source); + mNotificationBuilder.setLargeIcon(bitmap); + mNotificationManager.notify(NOTIFICATION_ID, mNotificationBuilder.build()); + } + } + }.execute(); + } + +} diff --git a/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/MusicPlayerActivity.java b/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/MusicPlayerActivity.java new file mode 100644 index 000000000..648d26896 --- /dev/null +++ b/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/MusicPlayerActivity.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.mediabrowserservice; + +import android.app.Activity; +import android.media.browse.MediaBrowser; +import android.media.session.MediaController; +import android.os.Bundle; + +/** + * Main activity for the music player. + */ +public class MusicPlayerActivity extends Activity + implements BrowseFragment.FragmentDataHelper { + + private static final String TAG = MusicPlayerActivity.class.getSimpleName(); + + private MediaBrowser mMediaBrowser; + private MediaController mMediaController; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_player); + if (savedInstanceState == null) { + getFragmentManager().beginTransaction() + .add(R.id.container, BrowseFragment.newInstance(null)) + .commit(); + } + } + + @Override + public void onMediaItemSelected(MediaBrowser.MediaItem item) { + if (item.isPlayable()) { + getMediaController().getTransportControls().playFromMediaId(item.getMediaId(), null); + QueueFragment queueFragment = QueueFragment.newInstance(); + getFragmentManager().beginTransaction() + .replace(R.id.container, queueFragment) + .addToBackStack(null) + .commit(); + } else if (item.isBrowsable()) { + getFragmentManager().beginTransaction() + .replace(R.id.container, BrowseFragment.newInstance(item.getMediaId())) + .addToBackStack(null) + .commit(); + } + } +} diff --git a/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/MusicService.java b/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/MusicService.java new file mode 100644 index 000000000..a7a9ae2e7 --- /dev/null +++ b/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/MusicService.java @@ -0,0 +1,936 @@ +/* + * Copyright (C) 2014 Google Inc. All Rights Reserved. + * + * 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.mediabrowserservice; + +import android.content.Context; +import android.content.Intent; +import android.media.AudioManager; +import android.media.MediaDescription; +import android.media.MediaMetadata; +import android.media.MediaPlayer; +import android.media.MediaPlayer.OnCompletionListener; +import android.media.MediaPlayer.OnErrorListener; +import android.media.MediaPlayer.OnPreparedListener; +import android.media.browse.MediaBrowser; +import android.media.browse.MediaBrowser.MediaItem; +import android.media.session.MediaSession; +import android.media.session.PlaybackState; +import android.net.Uri; +import android.net.wifi.WifiManager; +import android.net.wifi.WifiManager.WifiLock; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.os.PowerManager; +import android.os.SystemClock; +import android.service.media.MediaBrowserService; + +import com.example.android.mediabrowserservice.model.MusicProvider; +import com.example.android.mediabrowserservice.utils.LogHelper; +import com.example.android.mediabrowserservice.utils.MediaIDHelper; +import com.example.android.mediabrowserservice.utils.QueueHelper; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import static com.example.android.mediabrowserservice.utils.MediaIDHelper.MEDIA_ID_MUSICS_BY_GENRE; +import static com.example.android.mediabrowserservice.utils.MediaIDHelper.MEDIA_ID_ROOT; +import static com.example.android.mediabrowserservice.utils.MediaIDHelper.createBrowseCategoryMediaID; +import static com.example.android.mediabrowserservice.utils.MediaIDHelper.extractBrowseCategoryFromMediaID; + +/** + * This class provides a MediaBrowser through a service. It exposes the media library to a browsing + * client, through the onGetRoot and onLoadChildren methods. It also creates a MediaSession and + * exposes it through its MediaSession.Token, which allows the client to create a MediaController + * that connects to and send control commands to the MediaSession remotely. This is useful for + * user interfaces that need to interact with your media session, like Android Auto. You can + * (should) also use the same service from your app's UI, which gives a seamless playback + * experience to the user. + * + * To implement a MediaBrowserService, you need to: + * + *

    + * + *
  • Extend {@link android.service.media.MediaBrowserService}, implementing the media browsing + * related methods {@link android.service.media.MediaBrowserService#onGetRoot} and + * {@link android.service.media.MediaBrowserService#onLoadChildren}; + *
  • In onCreate, start a new {@link android.media.session.MediaSession} and notify its parent + * with the session's token {@link android.service.media.MediaBrowserService#setSessionToken}; + * + *
  • Set a callback on the + * {@link android.media.session.MediaSession#setCallback(android.media.session.MediaSession.Callback)}. + * The callback will receive all the user's actions, like play, pause, etc; + * + *
  • Handle all the actual music playing using any method your app prefers (for example, + * {@link android.media.MediaPlayer}) + * + *
  • Update playbackState, "now playing" metadata and queue, using MediaSession proper methods + * {@link android.media.session.MediaSession#setPlaybackState(android.media.session.PlaybackState)} + * {@link android.media.session.MediaSession#setMetadata(android.media.MediaMetadata)} and + * {@link android.media.session.MediaSession#setQueue(java.util.List)}) + * + *
  • Declare and export the service in AndroidManifest with an intent receiver for the action + * android.media.browse.MediaBrowserService + * + *
+ * + * To make your app compatible with Android Auto, you also need to: + * + *
    + * + *
  • Declare a meta-data tag in AndroidManifest.xml linking to a xml resource + * with a <automotiveApp> root element. For a media app, this must include + * an <uses name="media"/> element as a child. + * For example, in AndroidManifest.xml: + * <meta-data android:name="com.google.android.gms.car.application" + * android:resource="@xml/automotive_app_desc"/> + * And in res/values/automotive_app_desc.xml: + * <automotiveApp> + * <uses name="media"/> + * </automotiveApp> + * + *
+ + * @see README.md for more details. + * + */ + +public class MusicService extends MediaBrowserService implements OnPreparedListener, + OnCompletionListener, OnErrorListener, AudioManager.OnAudioFocusChangeListener { + + private static final String TAG = "MusicService"; + + // Action to thumbs up a media item + private static final String CUSTOM_ACTION_THUMBS_UP = "thumbs_up"; + // Delay stopSelf by using a handler. + private static final int STOP_DELAY = 30000; + + // The volume we set the media player to when we lose audio focus, but are + // allowed to reduce the volume instead of stopping playback. + public static final float VOLUME_DUCK = 0.2f; + + // The volume we set the media player when we have audio focus. + public static final float VOLUME_NORMAL = 1.0f; + public static final String ANDROID_AUTO_PACKAGE_NAME = "com.google.android.projection.gearhead"; + public static final String ANDROID_AUTO_EMULATOR_PACKAGE_NAME = "com.google.android.mediasimulator"; + + // Music catalog manager + private MusicProvider mMusicProvider; + + private MediaSession mSession; + private MediaPlayer mMediaPlayer; + + // "Now playing" queue: + private List mPlayingQueue; + private int mCurrentIndexOnQueue; + + // Current local media player state + private int mState = PlaybackState.STATE_NONE; + + // Wifi lock that we hold when streaming files from the internet, in order + // to prevent the device from shutting off the Wifi radio + private WifiLock mWifiLock; + + private MediaNotification mMediaNotification; + + // Indicates whether the service was started. + private boolean mServiceStarted; + + enum AudioFocus { + NoFocusNoDuck, // we don't have audio focus, and can't duck + NoFocusCanDuck, // we don't have focus, but can play at a low volume + // ("ducking") + Focused // we have full audio focus + } + + // Type of audio focus we have: + private AudioFocus mAudioFocus = AudioFocus.NoFocusNoDuck; + private AudioManager mAudioManager; + + // Indicates if we should start playing immediately after we gain focus. + private boolean mPlayOnFocusGain; + + private Handler mDelayedStopHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + if ((mMediaPlayer != null && mMediaPlayer.isPlaying()) || + mPlayOnFocusGain) { + LogHelper.d(TAG, "Ignoring delayed stop since the media player is in use."); + return; + } + LogHelper.d(TAG, "Stopping service with delay handler."); + stopSelf(); + mServiceStarted = false; + } + }; + + /* + * (non-Javadoc) + * @see android.app.Service#onCreate() + */ + @Override + public void onCreate() { + super.onCreate(); + LogHelper.d(TAG, "onCreate"); + + mPlayingQueue = new ArrayList<>(); + + // Create the Wifi lock (this does not acquire the lock, this just creates it) + mWifiLock = ((WifiManager) getSystemService(Context.WIFI_SERVICE)) + .createWifiLock(WifiManager.WIFI_MODE_FULL, "MusicDemo_lock"); + + + // Create the music catalog metadata provider + mMusicProvider = new MusicProvider(); + mMusicProvider.retrieveMedia(new MusicProvider.Callback() { + @Override + public void onMusicCatalogReady(boolean success) { + mState = success ? PlaybackState.STATE_NONE : PlaybackState.STATE_ERROR; + } + }); + + mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); + + // Start a new MediaSession + mSession = new MediaSession(this, "MusicService"); + setSessionToken(mSession.getSessionToken()); + mSession.setCallback(new MediaSessionCallback()); + mSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS | + MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS); + + // Use these extras to reserve space for the corresponding actions, even when they are disabled + // in the playbackstate, so the custom actions don't reflow. + Bundle extras = new Bundle(); + extras.putBoolean( + "com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_NEXT", + true); + extras.putBoolean( + "com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_PREVIOUS", + true); + // If you want to reserve the Queue slot when there is no queue + // (mSession.setQueue(emptylist)), uncomment the lines below: + // extras.putBoolean( + // "com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_QUEUE", + // true); + mSession.setExtras(extras); + + updatePlaybackState(null); + + mMediaNotification = new MediaNotification(this); + } + + /* + * (non-Javadoc) + * @see android.app.Service#onDestroy() + */ + @Override + public void onDestroy() { + LogHelper.d(TAG, "onDestroy"); + + // Service is being killed, so make sure we release our resources + handleStopRequest(null); + + mDelayedStopHandler.removeCallbacksAndMessages(null); + // In particular, always release the MediaSession to clean up resources + // and notify associated MediaController(s). + mSession.release(); + } + + + @Override + public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) { + LogHelper.d(TAG, "OnGetRoot: clientPackageName=" + clientPackageName, + "; clientUid=" + clientUid + " ; rootHints=", rootHints); + // To ensure you are not allowing any arbitrary app to browse your app's contents, you + // need to check the origin: + if (!ANDROID_AUTO_PACKAGE_NAME.equals(clientPackageName) && + !ANDROID_AUTO_EMULATOR_PACKAGE_NAME.equals(clientPackageName) && + !getApplication().getPackageName().equals(clientPackageName)) { + // If the request comes from an untrusted package, return null. No further calls will + // be made to other media browsing methods. + LogHelper.w(TAG, "OnGetRoot: IGNORING request from untrusted package " + clientPackageName); + return null; + } + if (ANDROID_AUTO_PACKAGE_NAME.equals(clientPackageName)) { + // Optional: if your app needs to adapt ads, music library or anything else that + // needs to run differently when connected to the car, this is where you should handle + // it. + } + return new BrowserRoot(MEDIA_ID_ROOT, null); + } + + @Override + public void onLoadChildren(final String parentMediaId, final Result> result) { + if (!mMusicProvider.isInitialized()) { + // Use result.detach to allow calling result.sendResult from another thread: + result.detach(); + + mMusicProvider.retrieveMedia(new MusicProvider.Callback() { + @Override + public void onMusicCatalogReady(boolean success) { + if (success) { + loadChildrenImpl(parentMediaId, result); + } else { + updatePlaybackState(getString(R.string.error_no_metadata)); + result.sendResult(new ArrayList()); + } + } + }); + + } else { + // If our music catalog is already loaded/cached, load them into result immediately + loadChildrenImpl(parentMediaId, result); + } + } + + /** + * Actual implementation of onLoadChildren that assumes that MusicProvider is already + * initialized. + */ + private void loadChildrenImpl(final String parentMediaId, + final Result> result) { + LogHelper.d(TAG, "OnLoadChildren: parentMediaId=", parentMediaId); + + List mediaItems = new ArrayList<>(); + + if (MEDIA_ID_ROOT.equals(parentMediaId)) { + LogHelper.d(TAG, "OnLoadChildren.ROOT"); + mediaItems.add(new MediaBrowser.MediaItem( + new MediaDescription.Builder() + .setMediaId(MEDIA_ID_MUSICS_BY_GENRE) + .setTitle(getString(R.string.browse_genres)) + .setIconUri(Uri.parse("android.resource://" + + "com.example.android.mediabrowserservice/drawable/ic_by_genre")) + .setSubtitle(getString(R.string.browse_genre_subtitle)) + .build(), MediaBrowser.MediaItem.FLAG_BROWSABLE + )); + + } else if (MEDIA_ID_MUSICS_BY_GENRE.equals(parentMediaId)) { + LogHelper.d(TAG, "OnLoadChildren.GENRES"); + for (String genre: mMusicProvider.getGenres()) { + MediaBrowser.MediaItem item = new MediaBrowser.MediaItem( + new MediaDescription.Builder() + .setMediaId(createBrowseCategoryMediaID(MEDIA_ID_MUSICS_BY_GENRE, genre)) + .setTitle(genre) + .setSubtitle(getString(R.string.browse_musics_by_genre_subtitle, genre)) + .build(), MediaBrowser.MediaItem.FLAG_BROWSABLE + ); + mediaItems.add(item); + } + + } else if (parentMediaId.startsWith(MEDIA_ID_MUSICS_BY_GENRE)) { + String genre = extractBrowseCategoryFromMediaID(parentMediaId)[1]; + LogHelper.d(TAG, "OnLoadChildren.SONGS_BY_GENRE genre=", genre); + for (MediaMetadata track: mMusicProvider.getMusicsByGenre(genre)) { + // Since mediaMetadata fields are immutable, we need to create a copy, so we + // can set a hierarchy-aware mediaID. We will need to know the media hierarchy + // when we get a onPlayFromMusicID call, so we can create the proper queue based + // on where the music was selected from (by artist, by genre, random, etc) + String hierarchyAwareMediaID = MediaIDHelper.createTrackMediaID( + MEDIA_ID_MUSICS_BY_GENRE, genre, track); + MediaMetadata trackCopy = new MediaMetadata.Builder(track) + .putString(MediaMetadata.METADATA_KEY_MEDIA_ID, hierarchyAwareMediaID) + .build(); + MediaBrowser.MediaItem bItem = new MediaBrowser.MediaItem( + trackCopy.getDescription(), MediaItem.FLAG_PLAYABLE); + mediaItems.add(bItem); + } + } else { + LogHelper.w(TAG, "Skipping unmatched parentMediaId: ", parentMediaId); + } + result.sendResult(mediaItems); + } + + + + private final class MediaSessionCallback extends MediaSession.Callback { + @Override + public void onPlay() { + LogHelper.d(TAG, "play"); + + if (mPlayingQueue == null || mPlayingQueue.isEmpty()) { + mPlayingQueue = QueueHelper.getRandomQueue(mMusicProvider); + mSession.setQueue(mPlayingQueue); + mSession.setQueueTitle(getString(R.string.random_queue_title)); + // start playing from the beginning of the queue + mCurrentIndexOnQueue = 0; + } + + if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) { + handlePlayRequest(); + } + } + + @Override + public void onSkipToQueueItem(long queueId) { + LogHelper.d(TAG, "OnSkipToQueueItem:" + queueId); + if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) { + + // set the current index on queue from the music Id: + mCurrentIndexOnQueue = QueueHelper.getMusicIndexOnQueue(mPlayingQueue, queueId); + + // play the music + handlePlayRequest(); + } + } + + @Override + public void onPlayFromMediaId(String mediaId, Bundle extras) { + LogHelper.d(TAG, "playFromMediaId mediaId:", mediaId, " extras=", extras); + + // The mediaId used here is not the unique musicId. This one comes from the + // MediaBrowser, and is actually a "hierarchy-aware mediaID": a concatenation of + // the hierarchy in MediaBrowser and the actual unique musicID. This is necessary + // so we can build the correct playing queue, based on where the track was + // selected from. + mPlayingQueue = QueueHelper.getPlayingQueue(mediaId, mMusicProvider); + mSession.setQueue(mPlayingQueue); + String queueTitle = getString(R.string.browse_musics_by_genre_subtitle, + MediaIDHelper.extractBrowseCategoryValueFromMediaID(mediaId)); + mSession.setQueueTitle(queueTitle); + + if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) { + String uniqueMusicID = MediaIDHelper.extractMusicIDFromMediaID(mediaId); + // set the current index on queue from the music Id: + mCurrentIndexOnQueue = QueueHelper.getMusicIndexOnQueue( + mPlayingQueue, uniqueMusicID); + + // play the music + handlePlayRequest(); + } + } + + @Override + public void onPause() { + LogHelper.d(TAG, "pause. current state=" + mState); + handlePauseRequest(); + } + + @Override + public void onStop() { + LogHelper.d(TAG, "stop. current state=" + mState); + handleStopRequest(null); + } + + @Override + public void onSkipToNext() { + LogHelper.d(TAG, "skipToNext"); + mCurrentIndexOnQueue++; + if (mPlayingQueue != null && mCurrentIndexOnQueue >= mPlayingQueue.size()) { + mCurrentIndexOnQueue = 0; + } + if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) { + mState = PlaybackState.STATE_PLAYING; + handlePlayRequest(); + } else { + LogHelper.e(TAG, "skipToNext: cannot skip to next. next Index=" + + mCurrentIndexOnQueue + " queue length=" + + (mPlayingQueue == null ? "null" : mPlayingQueue.size())); + handleStopRequest("Cannot skip"); + } + } + + @Override + public void onSkipToPrevious() { + LogHelper.d(TAG, "skipToPrevious"); + + mCurrentIndexOnQueue--; + if (mPlayingQueue != null && mCurrentIndexOnQueue < 0) { + // This sample's behavior: skipping to previous when in first song restarts the + // first song. + mCurrentIndexOnQueue = 0; + } + if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) { + mState = PlaybackState.STATE_PLAYING; + handlePlayRequest(); + } else { + LogHelper.e(TAG, "skipToPrevious: cannot skip to previous. previous Index=" + + mCurrentIndexOnQueue + " queue length=" + + (mPlayingQueue == null ? "null" : mPlayingQueue.size())); + handleStopRequest("Cannot skip"); + } + } + + @Override + public void onCustomAction(String action, Bundle extras) { + if (CUSTOM_ACTION_THUMBS_UP.equals(action)) { + LogHelper.i(TAG, "onCustomAction: favorite for current track"); + MediaMetadata track = getCurrentPlayingMusic(); + if (track != null) { + String mediaId = track.getString(MediaMetadata.METADATA_KEY_MEDIA_ID); + mMusicProvider.setFavorite(mediaId, !mMusicProvider.isFavorite(mediaId)); + } + updatePlaybackState(null); + } else { + LogHelper.e(TAG, "Unsupported action: ", action); + } + + } + + @Override + public void onPlayFromSearch(String query, Bundle extras) { + LogHelper.d(TAG, "playFromSearch query=", query); + + mPlayingQueue = QueueHelper.getPlayingQueueFromSearch(query, mMusicProvider); + LogHelper.d(TAG, "playFromSearch playqueue.length=" + mPlayingQueue.size()); + mSession.setQueue(mPlayingQueue); + + if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) { + + // start playing from the beginning of the queue + mCurrentIndexOnQueue = 0; + + handlePlayRequest(); + } + } + } + + + + /* + * Called when media player is done playing current song. + * @see android.media.MediaPlayer.OnCompletionListener + */ + @Override + public void onCompletion(MediaPlayer player) { + LogHelper.d(TAG, "onCompletion from MediaPlayer"); + // The media player finished playing the current song, so we go ahead + // and start the next. + if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) { + // In this sample, we restart the playing queue when it gets to the end: + mCurrentIndexOnQueue++; + if (mCurrentIndexOnQueue >= mPlayingQueue.size()) { + mCurrentIndexOnQueue = 0; + } + handlePlayRequest(); + } else { + // If there is nothing to play, we stop and release the resources: + handleStopRequest(null); + } + } + + /* + * Called when media player is done preparing. + * @see android.media.MediaPlayer.OnPreparedListener + */ + @Override + public void onPrepared(MediaPlayer player) { + LogHelper.d(TAG, "onPrepared from MediaPlayer"); + // The media player is done preparing. That means we can start playing if we + // have audio focus. + configMediaPlayerState(); + } + + /** + * Called when there's an error playing media. When this happens, the media + * player goes to the Error state. We warn the user about the error and + * reset the media player. + * + * @see android.media.MediaPlayer.OnErrorListener + */ + @Override + public boolean onError(MediaPlayer mp, int what, int extra) { + LogHelper.e(TAG, "Media player error: what=" + what + ", extra=" + extra); + handleStopRequest("MediaPlayer error " + what + " (" + extra + ")"); + return true; // true indicates we handled the error + } + + + + + /** + * Called by AudioManager on audio focus changes. + */ + @Override + public void onAudioFocusChange(int focusChange) { + LogHelper.d(TAG, "onAudioFocusChange. focusChange=" + focusChange); + if (focusChange == AudioManager.AUDIOFOCUS_GAIN) { + // We have gained focus: + mAudioFocus = AudioFocus.Focused; + + } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS || + focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT || + focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) { + // We have lost focus. If we can duck (low playback volume), we can keep playing. + // Otherwise, we need to pause the playback. + boolean canDuck = focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK; + mAudioFocus = canDuck ? AudioFocus.NoFocusCanDuck : AudioFocus.NoFocusNoDuck; + + // If we are playing, we need to reset media player by calling configMediaPlayerState + // with mAudioFocus properly set. + if (mState == PlaybackState.STATE_PLAYING && !canDuck) { + // If we don't have audio focus and can't duck, we save the information that + // we were playing, so that we can resume playback once we get the focus back. + mPlayOnFocusGain = true; + } + } else { + LogHelper.e(TAG, "onAudioFocusChange: Ignoring unsupported focusChange: " + focusChange); + } + + configMediaPlayerState(); + } + + + + /** + * Handle a request to play music + */ + private void handlePlayRequest() { + LogHelper.d(TAG, "handlePlayRequest: mState=" + mState); + + mDelayedStopHandler.removeCallbacksAndMessages(null); + if (!mServiceStarted) { + LogHelper.v(TAG, "Starting service"); + // The MusicService needs to keep running even after the calling MediaBrowser + // is disconnected. Call startService(Intent) and then stopSelf(..) when we no longer + // need to play media. + startService(new Intent(getApplicationContext(), MusicService.class)); + mServiceStarted = true; + } + + mPlayOnFocusGain = true; + tryToGetAudioFocus(); + + if (!mSession.isActive()) { + mSession.setActive(true); + } + + // actually play the song + if (mState == PlaybackState.STATE_PAUSED) { + // If we're paused, just continue playback and restore the + // 'foreground service' state. + configMediaPlayerState(); + } else { + // If we're stopped or playing a song, + // just go ahead to the new song and (re)start playing + playCurrentSong(); + } + } + + + /** + * Handle a request to pause music + */ + private void handlePauseRequest() { + LogHelper.d(TAG, "handlePauseRequest: mState=" + mState); + + if (mState == PlaybackState.STATE_PLAYING) { + // Pause media player and cancel the 'foreground service' state. + mState = PlaybackState.STATE_PAUSED; + if (mMediaPlayer.isPlaying()) { + mMediaPlayer.pause(); + } + // while paused, retain the MediaPlayer but give up audio focus + relaxResources(false); + giveUpAudioFocus(); + } + updatePlaybackState(null); + } + + /** + * Handle a request to stop music + */ + private void handleStopRequest(String withError) { + LogHelper.d(TAG, "handleStopRequest: mState=" + mState + " error=", withError); + mState = PlaybackState.STATE_STOPPED; + + // let go of all resources... + relaxResources(true); + giveUpAudioFocus(); + updatePlaybackState(withError); + + mMediaNotification.stopNotification(); + + // service is no longer necessary. Will be started again if needed. + stopSelf(); + mServiceStarted = false; + } + + /** + * Releases resources used by the service for playback. This includes the + * "foreground service" status, the wake locks and possibly the MediaPlayer. + * + * @param releaseMediaPlayer Indicates whether the Media Player should also + * be released or not + */ + private void relaxResources(boolean releaseMediaPlayer) { + LogHelper.d(TAG, "relaxResources. releaseMediaPlayer=" + releaseMediaPlayer); + // stop being a foreground service + stopForeground(true); + + // reset the delayed stop handler. + mDelayedStopHandler.removeCallbacksAndMessages(null); + mDelayedStopHandler.sendEmptyMessageDelayed(0, STOP_DELAY); + + // stop and release the Media Player, if it's available + if (releaseMediaPlayer && mMediaPlayer != null) { + mMediaPlayer.reset(); + mMediaPlayer.release(); + mMediaPlayer = null; + } + + // we can also release the Wifi lock, if we're holding it + if (mWifiLock.isHeld()) { + mWifiLock.release(); + } + } + + /** + * Reconfigures MediaPlayer according to audio focus settings and + * starts/restarts it. This method starts/restarts the MediaPlayer + * respecting the current audio focus state. So if we have focus, it will + * play normally; if we don't have focus, it will either leave the + * MediaPlayer paused or set it to a low volume, depending on what is + * allowed by the current focus settings. This method assumes mPlayer != + * null, so if you are calling it, you have to do so from a context where + * you are sure this is the case. + */ + private void configMediaPlayerState() { + LogHelper.d(TAG, "configAndStartMediaPlayer. mAudioFocus=" + mAudioFocus); + if (mAudioFocus == AudioFocus.NoFocusNoDuck) { + // If we don't have audio focus and can't duck, we have to pause, + if (mState == PlaybackState.STATE_PLAYING) { + handlePauseRequest(); + } + } else { // we have audio focus: + if (mAudioFocus == AudioFocus.NoFocusCanDuck) { + mMediaPlayer.setVolume(VOLUME_DUCK, VOLUME_DUCK); // we'll be relatively quiet + } else { + mMediaPlayer.setVolume(VOLUME_NORMAL, VOLUME_NORMAL); // we can be loud again + } + // If we were playing when we lost focus, we need to resume playing. + if (mPlayOnFocusGain) { + if (!mMediaPlayer.isPlaying()) { + LogHelper.d(TAG, "configAndStartMediaPlayer startMediaPlayer."); + mMediaPlayer.start(); + } + mPlayOnFocusGain = false; + mState = PlaybackState.STATE_PLAYING; + } + } + updatePlaybackState(null); + } + + /** + * Makes sure the media player exists and has been reset. This will create + * the media player if needed, or reset the existing media player if one + * already exists. + */ + private void createMediaPlayerIfNeeded() { + LogHelper.d(TAG, "createMediaPlayerIfNeeded. needed? " + (mMediaPlayer==null)); + if (mMediaPlayer == null) { + mMediaPlayer = new MediaPlayer(); + + // Make sure the media player will acquire a wake-lock while + // playing. If we don't do that, the CPU might go to sleep while the + // song is playing, causing playback to stop. + mMediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK); + + // we want the media player to notify us when it's ready preparing, + // and when it's done playing: + mMediaPlayer.setOnPreparedListener(this); + mMediaPlayer.setOnCompletionListener(this); + mMediaPlayer.setOnErrorListener(this); + } else { + mMediaPlayer.reset(); + } + } + + /** + * Starts playing the current song in the playing queue. + */ + void playCurrentSong() { + MediaMetadata track = getCurrentPlayingMusic(); + if (track == null) { + LogHelper.e(TAG, "playSong: ignoring request to play next song, because cannot" + + " find it." + + " currentIndex=" + mCurrentIndexOnQueue + + " playQueue.size=" + (mPlayingQueue==null?"null": mPlayingQueue.size())); + return; + } + String source = track.getString(MusicProvider.CUSTOM_METADATA_TRACK_SOURCE); + LogHelper.d(TAG, "playSong: current (" + mCurrentIndexOnQueue + ") in playingQueue. " + + " musicId=" + track.getString(MediaMetadata.METADATA_KEY_MEDIA_ID) + + " source=" + source); + + mState = PlaybackState.STATE_STOPPED; + relaxResources(false); // release everything except MediaPlayer + + try { + createMediaPlayerIfNeeded(); + + mState = PlaybackState.STATE_BUFFERING; + + mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); + mMediaPlayer.setDataSource(source); + + // Starts preparing the media player in the background. When + // it's done, it will call our OnPreparedListener (that is, + // the onPrepared() method on this class, since we set the + // listener to 'this'). Until the media player is prepared, + // we *cannot* call start() on it! + mMediaPlayer.prepareAsync(); + + // If we are streaming from the internet, we want to hold a + // Wifi lock, which prevents the Wifi radio from going to + // sleep while the song is playing. + mWifiLock.acquire(); + + updatePlaybackState(null); + updateMetadata(); + + } catch (IOException ex) { + LogHelper.e(TAG, ex, "IOException playing song"); + updatePlaybackState(ex.getMessage()); + } + } + + + + private void updateMetadata() { + if (!QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) { + LogHelper.e(TAG, "Can't retrieve current metadata."); + mState = PlaybackState.STATE_ERROR; + updatePlaybackState(getResources().getString(R.string.error_no_metadata)); + return; + } + MediaSession.QueueItem queueItem = mPlayingQueue.get(mCurrentIndexOnQueue); + String mediaId = queueItem.getDescription().getMediaId(); + MediaMetadata track = mMusicProvider.getMusic(mediaId); + String trackId = track.getString(MediaMetadata.METADATA_KEY_MEDIA_ID); + if (!mediaId.equals(trackId)) { + throw new IllegalStateException("track ID (" + trackId + ") " + + "should match mediaId (" + mediaId + ")"); + } + LogHelper.d(TAG, "Updating metadata for MusicID= " + mediaId); + mSession.setMetadata(track); + } + + + /** + * Update the current media player state, optionally showing an error message. + * + * @param error if not null, error message to present to the user. + * + */ + private void updatePlaybackState(String error) { + + LogHelper.d(TAG, "updatePlaybackState, setting session playback state to " + mState); + long position = PlaybackState.PLAYBACK_POSITION_UNKNOWN; + if (mMediaPlayer != null && mMediaPlayer.isPlaying()) { + position = mMediaPlayer.getCurrentPosition(); + } + PlaybackState.Builder stateBuilder = new PlaybackState.Builder() + .setActions(getAvailableActions()); + + setCustomAction(stateBuilder); + + // If there is an error message, send it to the playback state: + if (error != null) { + // Error states are really only supposed to be used for errors that cause playback to + // stop unexpectedly and persist until the user takes action to fix it. + stateBuilder.setErrorMessage(error); + mState = PlaybackState.STATE_ERROR; + } + stateBuilder.setState(mState, position, 1.0f, SystemClock.elapsedRealtime()); + + // Set the activeQueueItemId if the current index is valid. + if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) { + MediaSession.QueueItem item = mPlayingQueue.get(mCurrentIndexOnQueue); + stateBuilder.setActiveQueueItemId(item.getQueueId()); + } + + mSession.setPlaybackState(stateBuilder.build()); + + if (mState == PlaybackState.STATE_PLAYING || mState == PlaybackState.STATE_PAUSED) { + mMediaNotification.startNotification(); + } + } + + private void setCustomAction(PlaybackState.Builder stateBuilder) { + MediaMetadata currentMusic = getCurrentPlayingMusic(); + if (currentMusic != null) { + // Set appropriate "Favorite" icon on Custom action: + String mediaId = currentMusic.getString(MediaMetadata.METADATA_KEY_MEDIA_ID); + int favoriteIcon = R.drawable.ic_star_off; + if (mMusicProvider.isFavorite(mediaId)) { + favoriteIcon = R.drawable.ic_star_on; + } + LogHelper.d(TAG, "updatePlaybackState, setting Favorite custom action of music ", + mediaId, " current favorite=", mMusicProvider.isFavorite(mediaId)); + stateBuilder.addCustomAction(CUSTOM_ACTION_THUMBS_UP, getString(R.string.favorite), + favoriteIcon); + } + } + + private long getAvailableActions() { + long actions = PlaybackState.ACTION_PLAY | PlaybackState.ACTION_PLAY_FROM_MEDIA_ID | + PlaybackState.ACTION_PLAY_FROM_SEARCH; + if (mPlayingQueue == null || mPlayingQueue.isEmpty()) { + return actions; + } + if (mState == PlaybackState.STATE_PLAYING) { + actions |= PlaybackState.ACTION_PAUSE; + } + if (mCurrentIndexOnQueue > 0) { + actions |= PlaybackState.ACTION_SKIP_TO_PREVIOUS; + } + if (mCurrentIndexOnQueue < mPlayingQueue.size() - 1) { + actions |= PlaybackState.ACTION_SKIP_TO_NEXT; + } + return actions; + } + + private MediaMetadata getCurrentPlayingMusic() { + if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) { + MediaSession.QueueItem item = mPlayingQueue.get(mCurrentIndexOnQueue); + if (item != null) { + LogHelper.d(TAG, "getCurrentPlayingMusic for musicId=", + item.getDescription().getMediaId()); + return mMusicProvider.getMusic(item.getDescription().getMediaId()); + } + } + return null; + } + + /** + * Try to get the system audio focus. + */ + void tryToGetAudioFocus() { + LogHelper.d(TAG, "tryToGetAudioFocus"); + if (mAudioFocus != AudioFocus.Focused) { + int result = mAudioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC, + AudioManager.AUDIOFOCUS_GAIN); + if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { + mAudioFocus = AudioFocus.Focused; + } + } + } + + /** + * Give up the audio focus. + */ + void giveUpAudioFocus() { + LogHelper.d(TAG, "giveUpAudioFocus"); + if (mAudioFocus == AudioFocus.Focused) { + if (mAudioManager.abandonAudioFocus(this) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { + mAudioFocus = AudioFocus.NoFocusNoDuck; + } + } + } +} diff --git a/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/QueueAdapter.java b/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/QueueAdapter.java new file mode 100644 index 000000000..4f24e9944 --- /dev/null +++ b/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/QueueAdapter.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.mediabrowserservice; + +import android.app.Activity; +import android.media.session.MediaSession; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.TextView; + +import java.util.ArrayList; + +/** + * A list adapter for items in a queue + */ +public class QueueAdapter extends ArrayAdapter { + + // The currently selected/active queue item Id. + private long mActiveQueueItemId = MediaSession.QueueItem.UNKNOWN_ID; + + public QueueAdapter(Activity context) { + super(context, R.layout.media_list_item, new ArrayList()); + } + + public void setActiveQueueItemId(long id) { + this.mActiveQueueItemId = id; + } + + private static class ViewHolder { + ImageView mImageView; + TextView mTitleView; + TextView mDescriptionView; + } + + public View getView(int position, View convertView, ViewGroup parent) { + ViewHolder holder; + + if (convertView == null) { + convertView = LayoutInflater.from(getContext()) + .inflate(R.layout.media_list_item, parent, false); + holder = new ViewHolder(); + holder.mImageView = (ImageView) convertView.findViewById(R.id.play_eq); + holder.mTitleView = (TextView) convertView.findViewById(R.id.title); + holder.mDescriptionView = (TextView) convertView.findViewById(R.id.description); + convertView.setTag(holder); + } else { + holder = (ViewHolder) convertView.getTag(); + } + + MediaSession.QueueItem item = getItem(position); + holder.mTitleView.setText(item.getDescription().getTitle()); + if (item.getDescription().getDescription() != null) { + holder.mDescriptionView.setText(item.getDescription().getDescription()); + } + + // If the itemId matches the active Id then use a different icon + if (mActiveQueueItemId == item.getQueueId()) { + holder.mImageView.setImageDrawable( + getContext().getDrawable(R.drawable.ic_equalizer_white_24dp)); + } else { + holder.mImageView.setImageDrawable( + getContext().getDrawable(R.drawable.ic_play_arrow_white_24dp)); + } + return convertView; + } +} diff --git a/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/QueueFragment.java b/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/QueueFragment.java new file mode 100644 index 000000000..f6076bc88 --- /dev/null +++ b/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/QueueFragment.java @@ -0,0 +1,295 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.mediabrowserservice; + +import android.app.Fragment; +import android.content.ComponentName; +import android.media.browse.MediaBrowser; +import android.media.session.MediaController; +import android.media.session.MediaSession; +import android.media.session.PlaybackState; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ImageButton; +import android.widget.ListView; + +import com.example.android.mediabrowserservice.utils.LogHelper; + +import java.util.List; + +/** + * A class that shows the Media Queue to the user. + */ +public class QueueFragment extends Fragment { + + private static final String TAG = QueueFragment.class.getSimpleName(); + + private ImageButton mSkipNext; + private ImageButton mSkipPrevious; + private ImageButton mPlayPause; + + private MediaBrowser mMediaBrowser; + private MediaController.TransportControls mTransportControls; + private MediaController mMediaController; + private PlaybackState mPlaybackState; + + private QueueAdapter mQueueAdapter; + + private MediaBrowser.ConnectionCallback mConnectionCallback = + new MediaBrowser.ConnectionCallback() { + @Override + public void onConnected() { + LogHelper.d(TAG, "onConnected: session token ", mMediaBrowser.getSessionToken()); + + if (mMediaBrowser.getSessionToken() == null) { + throw new IllegalArgumentException("No Session token"); + } + + mMediaController = new MediaController(getActivity(), + mMediaBrowser.getSessionToken()); + mTransportControls = mMediaController.getTransportControls(); + mMediaController.registerCallback(mSessionCallback); + + getActivity().setMediaController(mMediaController); + mPlaybackState = mMediaController.getPlaybackState(); + + List queue = mMediaController.getQueue(); + if (queue != null) { + mQueueAdapter.clear(); + mQueueAdapter.notifyDataSetInvalidated(); + mQueueAdapter.addAll(queue); + mQueueAdapter.notifyDataSetChanged(); + } + onPlaybackStateChanged(mPlaybackState); + } + + @Override + public void onConnectionFailed() { + LogHelper.d(TAG, "onConnectionFailed"); + } + + @Override + public void onConnectionSuspended() { + LogHelper.d(TAG, "onConnectionSuspended"); + mMediaController.unregisterCallback(mSessionCallback); + mTransportControls = null; + mMediaController = null; + getActivity().setMediaController(null); + } + }; + + // Receive callbacks from the MediaController. Here we update our state such as which queue + // is being shown, the current title and description and the PlaybackState. + private MediaController.Callback mSessionCallback = new MediaController.Callback() { + + @Override + public void onSessionDestroyed() { + LogHelper.d(TAG, "Session destroyed. Need to fetch a new Media Session"); + } + + @Override + public void onPlaybackStateChanged(PlaybackState state) { + if (state == null) { + return; + } + LogHelper.d(TAG, "Received playback state change to state ", state.getState()); + mPlaybackState = state; + QueueFragment.this.onPlaybackStateChanged(state); + } + + @Override + public void onQueueChanged(List queue) { + LogHelper.d(TAG, "onQueueChanged ", queue); + if (queue != null) { + mQueueAdapter.clear(); + mQueueAdapter.notifyDataSetInvalidated(); + mQueueAdapter.addAll(queue); + mQueueAdapter.notifyDataSetChanged(); + } + } + }; + + public static QueueFragment newInstance() { + return new QueueFragment(); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View rootView = inflater.inflate(R.layout.fragment_list, container, false); + + mSkipPrevious = (ImageButton) rootView.findViewById(R.id.skip_previous); + mSkipPrevious.setEnabled(false); + mSkipPrevious.setOnClickListener(mButtonListener); + + mSkipNext = (ImageButton) rootView.findViewById(R.id.skip_next); + mSkipNext.setEnabled(false); + mSkipNext.setOnClickListener(mButtonListener); + + mPlayPause = (ImageButton) rootView.findViewById(R.id.play_pause); + mPlayPause.setEnabled(true); + mPlayPause.setOnClickListener(mButtonListener); + + mQueueAdapter = new QueueAdapter(getActivity()); + + ListView mListView = (ListView) rootView.findViewById(R.id.list_view); + mListView.setAdapter(mQueueAdapter); + mListView.setFocusable(true); + mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + MediaSession.QueueItem item = mQueueAdapter.getItem(position); + mTransportControls.skipToQueueItem(item.getQueueId()); + } + }); + + mMediaBrowser = new MediaBrowser(getActivity(), + new ComponentName(getActivity(), MusicService.class), + mConnectionCallback, null); + + return rootView; + } + + @Override + public void onResume() { + super.onResume(); + if (mMediaBrowser != null) { + mMediaBrowser.connect(); + } + } + + @Override + public void onPause() { + super.onPause(); + if (mMediaController != null) { + mMediaController.unregisterCallback(mSessionCallback); + } + if (mMediaBrowser != null) { + mMediaBrowser.disconnect(); + } + } + + + private void onPlaybackStateChanged(PlaybackState state) { + LogHelper.d(TAG, "onPlaybackStateChanged ", state); + if (state == null) { + return; + } + mQueueAdapter.setActiveQueueItemId(state.getActiveQueueItemId()); + mQueueAdapter.notifyDataSetChanged(); + boolean enablePlay = false; + StringBuilder statusBuilder = new StringBuilder(); + switch (state.getState()) { + case PlaybackState.STATE_PLAYING: + statusBuilder.append("playing"); + enablePlay = false; + break; + case PlaybackState.STATE_PAUSED: + statusBuilder.append("paused"); + enablePlay = true; + break; + case PlaybackState.STATE_STOPPED: + statusBuilder.append("ended"); + enablePlay = true; + break; + case PlaybackState.STATE_ERROR: + statusBuilder.append("error: ").append(state.getErrorMessage()); + break; + case PlaybackState.STATE_BUFFERING: + statusBuilder.append("buffering"); + break; + case PlaybackState.STATE_NONE: + statusBuilder.append("none"); + enablePlay = false; + break; + case PlaybackState.STATE_CONNECTING: + statusBuilder.append("connecting"); + break; + default: + statusBuilder.append(mPlaybackState); + } + statusBuilder.append(" -- At position: ").append(state.getPosition()); + LogHelper.d(TAG, statusBuilder.toString()); + + if (enablePlay) { + mPlayPause.setImageDrawable( + getActivity().getDrawable(R.drawable.ic_play_arrow_white_24dp)); + } else { + mPlayPause.setImageDrawable(getActivity().getDrawable(R.drawable.ic_pause_white_24dp)); + } + + mSkipPrevious.setEnabled((state.getActions() & PlaybackState.ACTION_SKIP_TO_PREVIOUS) != 0); + mSkipNext.setEnabled((state.getActions() & PlaybackState.ACTION_SKIP_TO_NEXT) != 0); + + LogHelper.d(TAG, "Queue From MediaController *** Title " + + mMediaController.getQueueTitle() + "\n: Queue: " + mMediaController.getQueue() + + "\n Metadata " + mMediaController.getMetadata()); + } + + private View.OnClickListener mButtonListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + final int state = mPlaybackState == null ? + PlaybackState.STATE_NONE : mPlaybackState.getState(); + switch (v.getId()) { + case R.id.play_pause: + LogHelper.d(TAG, "Play button pressed, in state " + state); + if (state == PlaybackState.STATE_PAUSED || + state == PlaybackState.STATE_STOPPED || + state == PlaybackState.STATE_NONE) { + playMedia(); + } else if (state == PlaybackState.STATE_PLAYING) { + pauseMedia(); + } + break; + case R.id.skip_previous: + LogHelper.d(TAG, "Start button pressed, in state " + state); + skipToPrevious(); + break; + case R.id.skip_next: + skipToNext(); + break; + } + } + }; + + private void playMedia() { + if (mTransportControls != null) { + mTransportControls.play(); + } + } + + private void pauseMedia() { + if (mTransportControls != null) { + mTransportControls.pause(); + } + } + + private void skipToPrevious() { + if (mTransportControls != null) { + mTransportControls.skipToPrevious(); + } + } + + private void skipToNext() { + if (mTransportControls != null) { + mTransportControls.skipToNext(); + } + } +} diff --git a/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/model/MusicProvider.java b/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/model/MusicProvider.java new file mode 100644 index 000000000..ae90fb092 --- /dev/null +++ b/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/model/MusicProvider.java @@ -0,0 +1,296 @@ +/* + * Copyright (C) 2014 Google Inc. All Rights Reserved. + * + * 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.mediabrowserservice.model; + +import android.media.MediaMetadata; +import android.os.AsyncTask; + +import com.example.android.mediabrowserservice.utils.LogHelper; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URLConnection; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Utility class to get a list of MusicTrack's based on a server-side JSON + * configuration. + */ +public class MusicProvider { + + private static final String TAG = "MusicProvider"; + + private static final String CATALOG_URL = "http://storage.googleapis.com/automotive-media/music.json"; + + public static final String CUSTOM_METADATA_TRACK_SOURCE = "__SOURCE__"; + + private static String JSON_MUSIC = "music"; + private static String JSON_TITLE = "title"; + private static String JSON_ALBUM = "album"; + private static String JSON_ARTIST = "artist"; + private static String JSON_GENRE = "genre"; + private static String JSON_SOURCE = "source"; + private static String JSON_IMAGE = "image"; + private static String JSON_TRACK_NUMBER = "trackNumber"; + private static String JSON_TOTAL_TRACK_COUNT = "totalTrackCount"; + private static String JSON_DURATION = "duration"; + + private final ReentrantLock initializationLock = new ReentrantLock(); + + // Categorized caches for music track data: + private final HashMap> mMusicListByGenre; + private final HashMap mMusicListById; + + private final HashSet mFavoriteTracks; + + enum State { + NON_INITIALIZED, INITIALIZING, INITIALIZED; + } + + private State mCurrentState = State.NON_INITIALIZED; + + + public interface Callback { + void onMusicCatalogReady(boolean success); + } + + public MusicProvider() { + mMusicListByGenre = new HashMap<>(); + mMusicListById = new HashMap<>(); + mFavoriteTracks = new HashSet<>(); + } + + /** + * Get an iterator over the list of genres + * + * @return + */ + public Iterable getGenres() { + if (mCurrentState != State.INITIALIZED) { + return new ArrayList(0); + } + return mMusicListByGenre.keySet(); + } + + /** + * Get music tracks of the given genre + * + * @return + */ + public Iterable getMusicsByGenre(String genre) { + if (mCurrentState != State.INITIALIZED || !mMusicListByGenre.containsKey(genre)) { + return new ArrayList(); + } + return mMusicListByGenre.get(genre); + } + + /** + * Very basic implementation of a search that filter music tracks which title containing + * the given query. + * + * @return + */ + public Iterable searchMusics(String titleQuery) { + ArrayList result = new ArrayList<>(); + if (mCurrentState != State.INITIALIZED) { + return result; + } + titleQuery = titleQuery.toLowerCase(); + for (MediaMetadata track: mMusicListById.values()) { + if (track.getString(MediaMetadata.METADATA_KEY_TITLE).toLowerCase() + .contains(titleQuery)) { + result.add(track); + } + } + return result; + } + + public MediaMetadata getMusic(String mediaId) { + return mMusicListById.get(mediaId); + } + + public void setFavorite(String mediaId, boolean favorite) { + if (favorite) { + mFavoriteTracks.add(mediaId); + } else { + mFavoriteTracks.remove(mediaId); + } + } + + public boolean isFavorite(String musicId) { + return mFavoriteTracks.contains(musicId); + } + + public boolean isInitialized() { + return mCurrentState == State.INITIALIZED; + } + + /** + * Get the list of music tracks from a server and caches the track information + * for future reference, keying tracks by mediaId and grouping by genre. + * + * @return + */ + public void retrieveMedia(final Callback callback) { + + if (mCurrentState == State.INITIALIZED) { + // Nothing to do, execute callback immediately + callback.onMusicCatalogReady(true); + return; + } + + // Asynchronously load the music catalog in a separate thread + new AsyncTask() { + @Override + protected Object doInBackground(Object[] objects) { + retrieveMediaAsync(callback); + return null; + } + }.execute(); + } + + private void retrieveMediaAsync(Callback callback) { + initializationLock.lock(); + + try { + if (mCurrentState == State.NON_INITIALIZED) { + mCurrentState = State.INITIALIZING; + + int slashPos = CATALOG_URL.lastIndexOf('/'); + String path = CATALOG_URL.substring(0, slashPos + 1); + JSONObject jsonObj = parseUrl(CATALOG_URL); + + JSONArray tracks = jsonObj.getJSONArray(JSON_MUSIC); + if (tracks != null) { + for (int j = 0; j < tracks.length(); j++) { + MediaMetadata item = buildFromJSON(tracks.getJSONObject(j), path); + String genre = item.getString(MediaMetadata.METADATA_KEY_GENRE); + List list = mMusicListByGenre.get(genre); + if (list == null) { + list = new ArrayList<>(); + } + list.add(item); + mMusicListByGenre.put(genre, list); + mMusicListById.put(item.getString(MediaMetadata.METADATA_KEY_MEDIA_ID), + item); + } + } + mCurrentState = State.INITIALIZED; + } + } catch (RuntimeException | JSONException e) { + LogHelper.e(TAG, e, "Could not retrieve music list"); + } finally { + if (mCurrentState != State.INITIALIZED) { + // Something bad happened, so we reset state to NON_INITIALIZED to allow + // retries (eg if the network connection is temporary unavailable) + mCurrentState = State.NON_INITIALIZED; + } + initializationLock.unlock(); + if (callback != null) { + callback.onMusicCatalogReady(mCurrentState == State.INITIALIZED); + } + } + } + + private MediaMetadata buildFromJSON(JSONObject json, String basePath) throws JSONException { + String title = json.getString(JSON_TITLE); + String album = json.getString(JSON_ALBUM); + String artist = json.getString(JSON_ARTIST); + String genre = json.getString(JSON_GENRE); + String source = json.getString(JSON_SOURCE); + String iconUrl = json.getString(JSON_IMAGE); + int trackNumber = json.getInt(JSON_TRACK_NUMBER); + int totalTrackCount = json.getInt(JSON_TOTAL_TRACK_COUNT); + int duration = json.getInt(JSON_DURATION) * 1000; // ms + + LogHelper.d(TAG, "Found music track: ", json); + + // Media is stored relative to JSON file + if (!source.startsWith("http")) { + source = basePath + source; + } + if (!iconUrl.startsWith("http")) { + iconUrl = basePath + iconUrl; + } + // Since we don't have a unique ID in the server, we fake one using the hashcode of + // the music source. In a real world app, this could come from the server. + String id = String.valueOf(source.hashCode()); + + // Adding the music source to the MediaMetadata (and consequently using it in the + // mediaSession.setMetadata) is not a good idea for a real world music app, because + // the session metadata can be accessed by notification listeners. This is done in this + // sample for convenience only. + return new MediaMetadata.Builder() + .putString(MediaMetadata.METADATA_KEY_MEDIA_ID, id) + .putString(CUSTOM_METADATA_TRACK_SOURCE, source) + .putString(MediaMetadata.METADATA_KEY_ALBUM, album) + .putString(MediaMetadata.METADATA_KEY_ARTIST, artist) + .putLong(MediaMetadata.METADATA_KEY_DURATION, duration) + .putString(MediaMetadata.METADATA_KEY_GENRE, genre) + .putString(MediaMetadata.METADATA_KEY_ALBUM_ART_URI, iconUrl) + .putString(MediaMetadata.METADATA_KEY_TITLE, title) + .putLong(MediaMetadata.METADATA_KEY_TRACK_NUMBER, trackNumber) + .putLong(MediaMetadata.METADATA_KEY_NUM_TRACKS, totalTrackCount) + .build(); + } + + /** + * Download a JSON file from a server, parse the content and return the JSON + * object. + * + * @param urlString + * @return + */ + private JSONObject parseUrl(String urlString) { + InputStream is = null; + try { + java.net.URL url = new java.net.URL(urlString); + URLConnection urlConnection = url.openConnection(); + is = new BufferedInputStream(urlConnection.getInputStream()); + BufferedReader reader = new BufferedReader(new InputStreamReader( + urlConnection.getInputStream(), "iso-8859-1")); + StringBuilder sb = new StringBuilder(); + String line = null; + while ((line = reader.readLine()) != null) { + sb.append(line); + } + return new JSONObject(sb.toString()); + } catch (Exception e) { + LogHelper.e(TAG, "Failed to parse the json for media list", e); + return null; + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException e) { + // ignore + } + } + } + } +} \ No newline at end of file diff --git a/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/utils/BitmapHelper.java b/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/utils/BitmapHelper.java new file mode 100644 index 000000000..5f0e76754 --- /dev/null +++ b/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/utils/BitmapHelper.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.mediabrowserservice.utils; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; + +public class BitmapHelper { + + // Bitmap size for album art in media notifications when there are more than 3 playback actions + public static final int MEDIA_ART_SMALL_WIDTH=64; + public static final int MEDIA_ART_SMALL_HEIGHT=64; + + // Bitmap size for album art in media notifications when there are no more than 3 playback actions + public static final int MEDIA_ART_BIG_WIDTH=128; + public static final int MEDIA_ART_BIG_HEIGHT=128; + + public static final Bitmap scaleBitmap(int scaleFactor, InputStream is) { + // Get the dimensions of the bitmap + BitmapFactory.Options bmOptions = new BitmapFactory.Options(); + + // Decode the image file into a Bitmap sized to fill the View + bmOptions.inJustDecodeBounds = false; + bmOptions.inSampleSize = scaleFactor; + + Bitmap bitmap = BitmapFactory.decodeStream(is, null, bmOptions); + return bitmap; + } + + public static final int findScaleFactor(int targetW, int targetH, InputStream is) { + // Get the dimensions of the bitmap + BitmapFactory.Options bmOptions = new BitmapFactory.Options(); + bmOptions.inJustDecodeBounds = true; + BitmapFactory.decodeStream(is, null, bmOptions); + int actualW = bmOptions.outWidth; + int actualH = bmOptions.outHeight; + + // Determine how much to scale down the image + return Math.min(actualW/targetW, actualH/targetH); + } + + public static final Bitmap fetchAndRescaleBitmap(String uri, int width, int height) + throws IOException { + URL url = new URL(uri); + HttpURLConnection httpConnection = (HttpURLConnection) url.openConnection(); + httpConnection.setDoInput(true); + httpConnection.connect(); + InputStream inputStream = httpConnection.getInputStream(); + int scaleFactor = findScaleFactor(width, height, inputStream); + + httpConnection = (HttpURLConnection) url.openConnection(); + httpConnection.setDoInput(true); + httpConnection.connect(); + inputStream = httpConnection.getInputStream(); + Bitmap bitmap = scaleBitmap(scaleFactor, inputStream); + return bitmap; + } + +} diff --git a/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/utils/LogHelper.java b/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/utils/LogHelper.java new file mode 100644 index 000000000..92b2e099e --- /dev/null +++ b/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/utils/LogHelper.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.mediabrowserservice.utils; + +import android.util.Log; + +public class LogHelper { + public static void v(String tag, Object... messages) { + log(tag, Log.VERBOSE, null, messages); + } + + public static void d(String tag, Object... messages) { + log(tag, Log.DEBUG, null, messages); + } + + public static void i(String tag, Object... messages) { + log(tag, Log.INFO, null, messages); + } + + public static void w(String tag, Object... messages) { + log(tag, Log.WARN, null, messages); + } + + public static void w(String tag, Throwable t, Object... messages) { + log(tag, Log.WARN, t, messages); + } + + public static void e(String tag, Object... messages) { + log(tag, Log.ERROR, null, messages); + } + + public static void e(String tag, Throwable t, Object... messages) { + log(tag, Log.ERROR, t, messages); + } + + public static void log(String tag, int level, Throwable t, Object... messages) { + if (messages != null && Log.isLoggable(tag, level)) { + String message; + if (messages.length == 1) { + message = messages[0] == null ? null : messages[0].toString(); + } else { + StringBuilder sb = new StringBuilder(); + for (Object m: messages) { + sb.append(m); + } + if (t != null) { + sb.append("\n").append(Log.getStackTraceString(t)); + } + message = sb.toString(); + } + Log.println(level, tag, message); + } + } +} diff --git a/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/utils/MediaIDHelper.java b/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/utils/MediaIDHelper.java new file mode 100644 index 000000000..68e6db9e3 --- /dev/null +++ b/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/utils/MediaIDHelper.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2014 Google Inc. All Rights Reserved. + * + * 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.mediabrowserservice.utils; + +import android.media.MediaMetadata; + +/** + * Utility class to help on queue related tasks. + */ +public class MediaIDHelper { + + private static final String TAG = "MediaIDHelper"; + + // Media IDs used on browseable items of MediaBrowser + public static final String MEDIA_ID_ROOT = "__ROOT__"; + public static final String MEDIA_ID_MUSICS_BY_GENRE = "__BY_GENRE__"; + + public static final String createTrackMediaID(String categoryType, String categoryValue, + MediaMetadata track) { + // MediaIDs are of the form /|, to make it easy to + // find the category (like genre) that a music was selected from, so we + // can correctly build the playing queue. This is specially useful when + // one music can appear in more than one list, like "by genre -> genre_1" + // and "by artist -> artist_1". + return categoryType + "/" + categoryValue + "|" + + track.getString(MediaMetadata.METADATA_KEY_MEDIA_ID); + } + + public static final String createBrowseCategoryMediaID(String categoryType, String categoryValue) { + return categoryType + "/" + categoryValue; + } + + /** + * Extracts unique musicID from the mediaID. mediaID is, by this sample's convention, a + * concatenation of category (eg "by_genre"), categoryValue (eg "Classical") and unique + * musicID. This is necessary so we know where the user selected the music from, when the music + * exists in more than one music list, and thus we are able to correctly build the playing queue. + * + * @param musicID + * @return + */ + public static final String extractMusicIDFromMediaID(String musicID) { + String[] segments = musicID.split("\\|", 2); + return segments.length == 2 ? segments[1] : null; + } + + /** + * Extracts category and categoryValue from the mediaID. mediaID is, by this sample's + * convention, a concatenation of category (eg "by_genre"), categoryValue (eg "Classical") and + * mediaID. This is necessary so we know where the user selected the music from, when the music + * exists in more than one music list, and thus we are able to correctly build the playing queue. + * + * @param mediaID + * @return + */ + public static final String[] extractBrowseCategoryFromMediaID(String mediaID) { + if (mediaID.indexOf('|') >= 0) { + mediaID = mediaID.split("\\|")[0]; + } + if (mediaID.indexOf('/') == 0) { + return new String[]{mediaID, null}; + } else { + return mediaID.split("/", 2); + } + } + + public static final String extractBrowseCategoryValueFromMediaID(String mediaID) { + String[] categoryAndValue = extractBrowseCategoryFromMediaID(mediaID); + if (categoryAndValue != null && categoryAndValue.length == 2) { + return categoryAndValue[1]; + } + return null; + } +} \ No newline at end of file diff --git a/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/utils/QueueHelper.java b/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/utils/QueueHelper.java new file mode 100644 index 000000000..abe3d34cd --- /dev/null +++ b/samples/browseable/MediaBrowserService/src/com.example.android.mediabrowserservice/utils/QueueHelper.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2014 Google Inc. All Rights Reserved. + * + * 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.mediabrowserservice.utils; + +import android.media.MediaMetadata; +import android.media.session.MediaSession; + +import com.example.android.mediabrowserservice.model.MusicProvider; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import static com.example.android.mediabrowserservice.utils.MediaIDHelper.MEDIA_ID_MUSICS_BY_GENRE; + +/** + * Utility class to help on queue related tasks. + */ +public class QueueHelper { + + private static final String TAG = "QueueHelper"; + + public static final List getPlayingQueue(String mediaId, + MusicProvider musicProvider) { + + // extract the category and unique music ID from the media ID: + String[] category = MediaIDHelper.extractBrowseCategoryFromMediaID(mediaId); + + // This sample only supports genre category. + if (!category[0].equals(MEDIA_ID_MUSICS_BY_GENRE) || category.length != 2) { + LogHelper.e(TAG, "Could not build a playing queue for this mediaId: ", mediaId); + return null; + } + + String categoryValue = category[1]; + LogHelper.e(TAG, "Creating playing queue for musics of genre ", categoryValue); + + List queue = convertToQueue( + musicProvider.getMusicsByGenre(categoryValue)); + + return queue; + } + + public static final List getPlayingQueueFromSearch(String query, + MusicProvider musicProvider) { + + LogHelper.e(TAG, "Creating playing queue for musics from search ", query); + + return convertToQueue(musicProvider.searchMusics(query)); + } + + + public static final int getMusicIndexOnQueue(Iterable queue, + String mediaId) { + int index = 0; + for (MediaSession.QueueItem item: queue) { + if (mediaId.equals(item.getDescription().getMediaId())) { + return index; + } + index++; + } + return -1; + } + + public static final int getMusicIndexOnQueue(Iterable queue, + long queueId) { + int index = 0; + for (MediaSession.QueueItem item: queue) { + if (queueId == item.getQueueId()) { + return index; + } + index++; + } + return -1; + } + + private static final List convertToQueue( + Iterable tracks) { + List queue = new ArrayList<>(); + int count = 0; + for (MediaMetadata track : tracks) { + // We don't expect queues to change after created, so we use the item index as the + // queueId. Any other number unique in the queue would work. + MediaSession.QueueItem item = new MediaSession.QueueItem( + track.getDescription(), count++); + queue.add(item); + } + return queue; + + } + + /** + * Create a random queue. For simplicity sake, instead of a random queue, we create a + * queue using the first genre, + * + * @param musicProvider + * @return + */ + public static final List getRandomQueue(MusicProvider musicProvider) { + Iterator genres = musicProvider.getGenres().iterator(); + if (!genres.hasNext()) { + return new ArrayList<>(); + } + String genre = genres.next(); + Iterable tracks = musicProvider.getMusicsByGenre(genre); + + return convertToQueue(tracks); + } + + + + public static final boolean isIndexPlayable(int index, List queue) { + return (queue != null && index >= 0 && index < queue.size()); + } +} diff --git a/samples/browseable/MediaEffects/AndroidManifest.xml b/samples/browseable/MediaEffects/AndroidManifest.xml new file mode 100644 index 000000000..556f3c196 --- /dev/null +++ b/samples/browseable/MediaEffects/AndroidManifest.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + diff --git a/samples/browseable/MediaEffects/_index.jd b/samples/browseable/MediaEffects/_index.jd new file mode 100644 index 000000000..76448c36d --- /dev/null +++ b/samples/browseable/MediaEffects/_index.jd @@ -0,0 +1,12 @@ +page.tags="MediaEffects" +sample.group=Media +@jd:body + +

+ + This sample shows how to use the Media Effects APIs that were introduced in Android 4.0. + These APIs let you apply effects to image frames represented as OpenGL ES 2.0 textures. + Image frames can be images loaded from disk, frames from the device\'s camera, or other + video streams. + +

diff --git a/samples/browseable/MediaEffects/res/drawable-hdpi/ic_launcher.png b/samples/browseable/MediaEffects/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 000000000..960dc8eb1 Binary files /dev/null and b/samples/browseable/MediaEffects/res/drawable-hdpi/ic_launcher.png differ diff --git a/samples/browseable/NavigationDrawer/res/drawable-xhdpi/sample_dashboard_item_background.9.png b/samples/browseable/MediaEffects/res/drawable-hdpi/tile.9.png similarity index 100% rename from samples/browseable/NavigationDrawer/res/drawable-xhdpi/sample_dashboard_item_background.9.png rename to samples/browseable/MediaEffects/res/drawable-hdpi/tile.9.png diff --git a/samples/browseable/MediaEffects/res/drawable-mdpi/ic_launcher.png b/samples/browseable/MediaEffects/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 000000000..9356f268e Binary files /dev/null and b/samples/browseable/MediaEffects/res/drawable-mdpi/ic_launcher.png differ diff --git a/samples/browseable/MediaEffects/res/drawable-nodpi/puppy.jpg b/samples/browseable/MediaEffects/res/drawable-nodpi/puppy.jpg new file mode 100644 index 000000000..ef79be200 Binary files /dev/null and b/samples/browseable/MediaEffects/res/drawable-nodpi/puppy.jpg differ diff --git a/samples/browseable/MediaEffects/res/drawable-xhdpi/ic_launcher.png b/samples/browseable/MediaEffects/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 000000000..230d5584d Binary files /dev/null and b/samples/browseable/MediaEffects/res/drawable-xhdpi/ic_launcher.png differ diff --git a/samples/browseable/MediaEffects/res/drawable-xxhdpi/ic_launcher.png b/samples/browseable/MediaEffects/res/drawable-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..c825d4e4e Binary files /dev/null and b/samples/browseable/MediaEffects/res/drawable-xxhdpi/ic_launcher.png differ diff --git a/samples/browseable/MediaEffects/res/layout-w720dp/activity_main.xml b/samples/browseable/MediaEffects/res/layout-w720dp/activity_main.xml new file mode 100755 index 000000000..c9a52f621 --- /dev/null +++ b/samples/browseable/MediaEffects/res/layout-w720dp/activity_main.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/browseable/MediaEffects/res/layout/activity_main.xml b/samples/browseable/MediaEffects/res/layout/activity_main.xml new file mode 100755 index 000000000..1ae4f981e --- /dev/null +++ b/samples/browseable/MediaEffects/res/layout/activity_main.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/samples/browseable/MediaEffects/res/layout/fragment_media_effects.xml b/samples/browseable/MediaEffects/res/layout/fragment_media_effects.xml new file mode 100644 index 000000000..4fb1ce1bc --- /dev/null +++ b/samples/browseable/MediaEffects/res/layout/fragment_media_effects.xml @@ -0,0 +1,26 @@ + + + + + + diff --git a/samples/browseable/MediaEffects/res/menu/main.xml b/samples/browseable/MediaEffects/res/menu/main.xml new file mode 100644 index 000000000..b49c2c526 --- /dev/null +++ b/samples/browseable/MediaEffects/res/menu/main.xml @@ -0,0 +1,21 @@ + + + + + diff --git a/samples/browseable/MediaEffects/res/menu/media_effects.xml b/samples/browseable/MediaEffects/res/menu/media_effects.xml new file mode 100644 index 000000000..c37a9ac6b --- /dev/null +++ b/samples/browseable/MediaEffects/res/menu/media_effects.xml @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/browseable/MediaEffects/res/values-sw600dp/template-dimens.xml b/samples/browseable/MediaEffects/res/values-sw600dp/template-dimens.xml new file mode 100644 index 000000000..22074a2bd --- /dev/null +++ b/samples/browseable/MediaEffects/res/values-sw600dp/template-dimens.xml @@ -0,0 +1,24 @@ + + + + + + + @dimen/margin_huge + @dimen/margin_medium + + diff --git a/samples/browseable/MediaEffects/res/values-sw600dp/template-styles.xml b/samples/browseable/MediaEffects/res/values-sw600dp/template-styles.xml new file mode 100644 index 000000000..03d197418 --- /dev/null +++ b/samples/browseable/MediaEffects/res/values-sw600dp/template-styles.xml @@ -0,0 +1,25 @@ + + + + + + + diff --git a/samples/browseable/MediaEffects/res/values-v11/template-styles.xml b/samples/browseable/MediaEffects/res/values-v11/template-styles.xml new file mode 100644 index 000000000..8c1ea66f2 --- /dev/null +++ b/samples/browseable/MediaEffects/res/values-v11/template-styles.xml @@ -0,0 +1,22 @@ + + + + + + + + + + diff --git a/samples/browseable/DataLayer/Application/src/com.example.android.common/activities/SampleActivityBase.java b/samples/browseable/MediaEffects/src/com.example.android.common/activities/SampleActivityBase.java similarity index 100% rename from samples/browseable/DataLayer/Application/src/com.example.android.common/activities/SampleActivityBase.java rename to samples/browseable/MediaEffects/src/com.example.android.common/activities/SampleActivityBase.java diff --git a/samples/browseable/DataLayer/Application/src/com.example.android.common/logger/Log.java b/samples/browseable/MediaEffects/src/com.example.android.common/logger/Log.java similarity index 100% rename from samples/browseable/DataLayer/Application/src/com.example.android.common/logger/Log.java rename to samples/browseable/MediaEffects/src/com.example.android.common/logger/Log.java diff --git a/samples/browseable/DataLayer/Application/src/com.example.android.common/logger/LogFragment.java b/samples/browseable/MediaEffects/src/com.example.android.common/logger/LogFragment.java similarity index 100% rename from samples/browseable/DataLayer/Application/src/com.example.android.common/logger/LogFragment.java rename to samples/browseable/MediaEffects/src/com.example.android.common/logger/LogFragment.java diff --git a/samples/browseable/DataLayer/Application/src/com.example.android.common/logger/LogNode.java b/samples/browseable/MediaEffects/src/com.example.android.common/logger/LogNode.java similarity index 100% rename from samples/browseable/DataLayer/Application/src/com.example.android.common/logger/LogNode.java rename to samples/browseable/MediaEffects/src/com.example.android.common/logger/LogNode.java diff --git a/samples/browseable/DataLayer/Application/src/com.example.android.common/logger/LogView.java b/samples/browseable/MediaEffects/src/com.example.android.common/logger/LogView.java similarity index 100% rename from samples/browseable/DataLayer/Application/src/com.example.android.common/logger/LogView.java rename to samples/browseable/MediaEffects/src/com.example.android.common/logger/LogView.java diff --git a/samples/browseable/DataLayer/Application/src/com.example.android.common/logger/LogWrapper.java b/samples/browseable/MediaEffects/src/com.example.android.common/logger/LogWrapper.java similarity index 100% rename from samples/browseable/DataLayer/Application/src/com.example.android.common/logger/LogWrapper.java rename to samples/browseable/MediaEffects/src/com.example.android.common/logger/LogWrapper.java diff --git a/samples/browseable/DataLayer/Application/src/com.example.android.common/logger/MessageOnlyLogFilter.java b/samples/browseable/MediaEffects/src/com.example.android.common/logger/MessageOnlyLogFilter.java similarity index 100% rename from samples/browseable/DataLayer/Application/src/com.example.android.common/logger/MessageOnlyLogFilter.java rename to samples/browseable/MediaEffects/src/com.example.android.common/logger/MessageOnlyLogFilter.java diff --git a/samples/browseable/MediaEffects/src/com.example.android.mediaeffects/GLToolbox.java b/samples/browseable/MediaEffects/src/com.example.android.mediaeffects/GLToolbox.java new file mode 100644 index 000000000..02a8c590d --- /dev/null +++ b/samples/browseable/MediaEffects/src/com.example.android.mediaeffects/GLToolbox.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.mediaeffects; + +import android.opengl.GLES20; + +public class GLToolbox { + + public static int loadShader(int shaderType, String source) { + int shader = GLES20.glCreateShader(shaderType); + if (shader != 0) { + GLES20.glShaderSource(shader, source); + GLES20.glCompileShader(shader); + int[] compiled = new int[1]; + GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0); + if (compiled[0] == 0) { + String info = GLES20.glGetShaderInfoLog(shader); + GLES20.glDeleteShader(shader); + throw new RuntimeException("Could not compile shader " + shaderType + ":" + info); + } + } + return shader; + } + + public static int createProgram(String vertexSource, String fragmentSource) { + int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource); + if (vertexShader == 0) { + return 0; + } + int pixelShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource); + if (pixelShader == 0) { + return 0; + } + + int program = GLES20.glCreateProgram(); + if (program != 0) { + GLES20.glAttachShader(program, vertexShader); + checkGlError("glAttachShader"); + GLES20.glAttachShader(program, pixelShader); + checkGlError("glAttachShader"); + GLES20.glLinkProgram(program); + int[] linkStatus = new int[1]; + GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, + 0); + if (linkStatus[0] != GLES20.GL_TRUE) { + String info = GLES20.glGetProgramInfoLog(program); + GLES20.glDeleteProgram(program); + throw new RuntimeException("Could not link program: " + info); + } + } + return program; + } + + public static void checkGlError(String op) { + int error; + while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) { + throw new RuntimeException(op + ": glError " + error); + } + } + + public static void initTexParams() { + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, + GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, + GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR); + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, + GLES20.GL_CLAMP_TO_EDGE); + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, + GLES20.GL_CLAMP_TO_EDGE); + } + +} diff --git a/samples/browseable/MediaEffects/src/com.example.android.mediaeffects/MainActivity.java b/samples/browseable/MediaEffects/src/com.example.android.mediaeffects/MainActivity.java new file mode 100644 index 000000000..be6224310 --- /dev/null +++ b/samples/browseable/MediaEffects/src/com.example.android.mediaeffects/MainActivity.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. +*/ + +package com.example.android.mediaeffects; + +import android.os.Bundle; +import android.support.v4.app.FragmentTransaction; +import android.view.Menu; +import android.view.MenuItem; +import android.widget.ViewAnimator; + +import com.example.android.common.activities.SampleActivityBase; +import com.example.android.common.logger.Log; +import com.example.android.common.logger.LogFragment; +import com.example.android.common.logger.LogWrapper; +import com.example.android.common.logger.MessageOnlyLogFilter; + +/** + * A simple launcher activity containing a summary sample description, sample log and a custom + * {@link android.support.v4.app.Fragment} which can display a view. + *

+ * For devices with displays with a width of 720dp or greater, the sample log is always visible, + * on other devices it's visibility is controlled by an item on the Action Bar. + */ +public class MainActivity extends SampleActivityBase { + + public static final String TAG = "MainActivity"; + + // Whether the Log Fragment is currently shown + private boolean mLogShown; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + if (savedInstanceState == null) { + FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); + MediaEffectsFragment fragment = new MediaEffectsFragment(); + transaction.replace(R.id.sample_content_fragment, fragment); + transaction.commit(); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.main, menu); + return true; + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + MenuItem logToggle = menu.findItem(R.id.menu_toggle_log); + logToggle.setVisible(findViewById(R.id.sample_output) instanceof ViewAnimator); + logToggle.setTitle(mLogShown ? R.string.sample_hide_log : R.string.sample_show_log); + + return super.onPrepareOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch(item.getItemId()) { + case R.id.menu_toggle_log: + mLogShown = !mLogShown; + ViewAnimator output = (ViewAnimator) findViewById(R.id.sample_output); + if (mLogShown) { + output.setDisplayedChild(1); + } else { + output.setDisplayedChild(0); + } + supportInvalidateOptionsMenu(); + return true; + } + return super.onOptionsItemSelected(item); + } + + /** Create a chain of targets that will receive log data */ + @Override + public void initializeLogging() { + // Wraps Android's native log framework. + LogWrapper logWrapper = new LogWrapper(); + // Using Log, front-end to the logging chain, emulates android.util.log method signatures. + Log.setLogNode(logWrapper); + + // Filter strips out everything except the message text. + MessageOnlyLogFilter msgFilter = new MessageOnlyLogFilter(); + logWrapper.setNext(msgFilter); + + // On screen logging via a fragment with a TextView. + LogFragment logFragment = (LogFragment) getSupportFragmentManager() + .findFragmentById(R.id.log_fragment); + msgFilter.setNext(logFragment.getLogView()); + + Log.i(TAG, "Ready"); + } +} diff --git a/samples/browseable/MediaEffects/src/com.example.android.mediaeffects/MediaEffectsFragment.java b/samples/browseable/MediaEffects/src/com.example.android.mediaeffects/MediaEffectsFragment.java new file mode 100644 index 000000000..5af16845f --- /dev/null +++ b/samples/browseable/MediaEffects/src/com.example.android.mediaeffects/MediaEffectsFragment.java @@ -0,0 +1,287 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.mediaeffects; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Color; +import android.media.effect.Effect; +import android.media.effect.EffectContext; +import android.media.effect.EffectFactory; +import android.opengl.GLES20; +import android.opengl.GLSurfaceView; +import android.opengl.GLUtils; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; + +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.opengles.GL10; + +public class MediaEffectsFragment extends Fragment implements GLSurfaceView.Renderer { + + private static final String STATE_CURRENT_EFFECT = "current_effect"; + + private GLSurfaceView mEffectView; + private int[] mTextures = new int[2]; + private EffectContext mEffectContext; + private Effect mEffect; + private TextureRenderer mTexRenderer = new TextureRenderer(); + private int mImageWidth; + private int mImageHeight; + private boolean mInitialized = false; + private int mCurrentEffect; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + } + + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_media_effects, container, false); + } + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + mEffectView = (GLSurfaceView) view.findViewById(R.id.effectsview); + mEffectView.setEGLContextClientVersion(2); + mEffectView.setRenderer(this); + mEffectView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); + if (null != savedInstanceState && savedInstanceState.containsKey(STATE_CURRENT_EFFECT)) { + setCurrentEffect(savedInstanceState.getInt(STATE_CURRENT_EFFECT)); + } else { + setCurrentEffect(R.id.none); + } + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.media_effects, menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + setCurrentEffect(item.getItemId()); + mEffectView.requestRender(); + return true; + } + + @Override + public void onSaveInstanceState(Bundle outState) { + outState.putInt(STATE_CURRENT_EFFECT, mCurrentEffect); + } + + @Override + public void onSurfaceCreated(GL10 gl, EGLConfig eglConfig) { + // Nothing to do here + } + + @Override + public void onSurfaceChanged(GL10 gl, int width, int height) { + if (mTexRenderer != null) { + mTexRenderer.updateViewSize(width, height); + } + } + + @Override + public void onDrawFrame(GL10 gl) { + if (!mInitialized) { + //Only need to do this once + mEffectContext = EffectContext.createWithCurrentGlContext(); + mTexRenderer.init(); + loadTextures(); + mInitialized = true; + } + if (mCurrentEffect != R.id.none) { + //if an effect is chosen initialize it and apply it to the texture + initEffect(); + applyEffect(); + } + renderResult(); + } + + private void setCurrentEffect(int effect) { + mCurrentEffect = effect; + } + + private void loadTextures() { + // Generate textures + GLES20.glGenTextures(2, mTextures, 0); + + // Load input bitmap + Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.puppy); + mImageWidth = bitmap.getWidth(); + mImageHeight = bitmap.getHeight(); + mTexRenderer.updateTextureSize(mImageWidth, mImageHeight); + + // Upload to texture + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextures[0]); + GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0); + + // Set texture parameters + GLToolbox.initTexParams(); + } + + private void initEffect() { + EffectFactory effectFactory = mEffectContext.getFactory(); + if (mEffect != null) { + mEffect.release(); + } + // Initialize the correct effect based on the selected menu/action item + switch (mCurrentEffect) { + + case R.id.none: + break; + + case R.id.autofix: + mEffect = effectFactory.createEffect(EffectFactory.EFFECT_AUTOFIX); + mEffect.setParameter("scale", 0.5f); + break; + + case R.id.bw: + mEffect = effectFactory.createEffect(EffectFactory.EFFECT_BLACKWHITE); + mEffect.setParameter("black", .1f); + mEffect.setParameter("white", .7f); + break; + + case R.id.brightness: + mEffect = effectFactory.createEffect(EffectFactory.EFFECT_BRIGHTNESS); + mEffect.setParameter("brightness", 2.0f); + break; + + case R.id.contrast: + mEffect = effectFactory.createEffect(EffectFactory.EFFECT_CONTRAST); + mEffect.setParameter("contrast", 1.4f); + break; + + case R.id.crossprocess: + mEffect = effectFactory.createEffect(EffectFactory.EFFECT_CROSSPROCESS); + break; + + case R.id.documentary: + mEffect = effectFactory.createEffect(EffectFactory.EFFECT_DOCUMENTARY); + break; + + case R.id.duotone: + mEffect = effectFactory.createEffect(EffectFactory.EFFECT_DUOTONE); + mEffect.setParameter("first_color", Color.YELLOW); + mEffect.setParameter("second_color", Color.DKGRAY); + break; + + case R.id.filllight: + mEffect = effectFactory.createEffect(EffectFactory.EFFECT_FILLLIGHT); + mEffect.setParameter("strength", .8f); + break; + + case R.id.fisheye: + mEffect = effectFactory.createEffect(EffectFactory.EFFECT_FISHEYE); + mEffect.setParameter("scale", .5f); + break; + + case R.id.flipvert: + mEffect = effectFactory.createEffect(EffectFactory.EFFECT_FLIP); + mEffect.setParameter("vertical", true); + break; + + case R.id.fliphor: + mEffect = effectFactory.createEffect(EffectFactory.EFFECT_FLIP); + mEffect.setParameter("horizontal", true); + break; + + case R.id.grain: + mEffect = effectFactory.createEffect(EffectFactory.EFFECT_GRAIN); + mEffect.setParameter("strength", 1.0f); + break; + + case R.id.grayscale: + mEffect = effectFactory.createEffect(EffectFactory.EFFECT_GRAYSCALE); + break; + + case R.id.lomoish: + mEffect = effectFactory.createEffect(EffectFactory.EFFECT_LOMOISH); + break; + + case R.id.negative: + mEffect = effectFactory.createEffect(EffectFactory.EFFECT_NEGATIVE); + break; + + case R.id.posterize: + mEffect = effectFactory.createEffect(EffectFactory.EFFECT_POSTERIZE); + break; + + case R.id.rotate: + mEffect = effectFactory.createEffect(EffectFactory.EFFECT_ROTATE); + mEffect.setParameter("angle", 180); + break; + + case R.id.saturate: + mEffect = effectFactory.createEffect(EffectFactory.EFFECT_SATURATE); + mEffect.setParameter("scale", .5f); + break; + + case R.id.sepia: + mEffect = effectFactory.createEffect(EffectFactory.EFFECT_SEPIA); + break; + + case R.id.sharpen: + mEffect = effectFactory.createEffect(EffectFactory.EFFECT_SHARPEN); + break; + + case R.id.temperature: + mEffect = effectFactory.createEffect(EffectFactory.EFFECT_TEMPERATURE); + mEffect.setParameter("scale", .9f); + break; + + case R.id.tint: + mEffect = effectFactory.createEffect(EffectFactory.EFFECT_TINT); + mEffect.setParameter("tint", Color.MAGENTA); + break; + + case R.id.vignette: + mEffect = effectFactory.createEffect(EffectFactory.EFFECT_VIGNETTE); + mEffect.setParameter("scale", .5f); + break; + + default: + break; + } + } + + private void applyEffect() { + mEffect.apply(mTextures[0], mImageWidth, mImageHeight, mTextures[1]); + } + + private void renderResult() { + if (mCurrentEffect != R.id.none) { + // if no effect is chosen, just render the original bitmap + mTexRenderer.renderTexture(mTextures[1]); + } else { + // render the result of applyEffect() + mTexRenderer.renderTexture(mTextures[0]); + } + } + +} diff --git a/samples/browseable/MediaEffects/src/com.example.android.mediaeffects/TextureRenderer.java b/samples/browseable/MediaEffects/src/com.example.android.mediaeffects/TextureRenderer.java new file mode 100644 index 000000000..9c77927d2 --- /dev/null +++ b/samples/browseable/MediaEffects/src/com.example.android.mediaeffects/TextureRenderer.java @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.mediaeffects; + +import android.opengl.GLES20; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; + +public class TextureRenderer { + + private int mProgram; + private int mTexSamplerHandle; + private int mTexCoordHandle; + private int mPosCoordHandle; + + private FloatBuffer mTexVertices; + private FloatBuffer mPosVertices; + + private int mViewWidth; + private int mViewHeight; + + private int mTexWidth; + private int mTexHeight; + + private static final String VERTEX_SHADER = + "attribute vec4 a_position;\n" + + "attribute vec2 a_texcoord;\n" + + "varying vec2 v_texcoord;\n" + + "void main() {\n" + + " gl_Position = a_position;\n" + + " v_texcoord = a_texcoord;\n" + + "}\n"; + + private static final String FRAGMENT_SHADER = + "precision mediump float;\n" + + "uniform sampler2D tex_sampler;\n" + + "varying vec2 v_texcoord;\n" + + "void main() {\n" + + " gl_FragColor = texture2D(tex_sampler, v_texcoord);\n" + + "}\n"; + + private static final float[] TEX_VERTICES = { + 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f + }; + + private static final float[] POS_VERTICES = { + -1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f + }; + + private static final int FLOAT_SIZE_BYTES = 4; + + public void init() { + // Create program + mProgram = GLToolbox.createProgram(VERTEX_SHADER, FRAGMENT_SHADER); + + // Bind attributes and uniforms + mTexSamplerHandle = GLES20.glGetUniformLocation(mProgram, + "tex_sampler"); + mTexCoordHandle = GLES20.glGetAttribLocation(mProgram, "a_texcoord"); + mPosCoordHandle = GLES20.glGetAttribLocation(mProgram, "a_position"); + + // Setup coordinate buffers + mTexVertices = ByteBuffer.allocateDirect( + TEX_VERTICES.length * FLOAT_SIZE_BYTES) + .order(ByteOrder.nativeOrder()).asFloatBuffer(); + mTexVertices.put(TEX_VERTICES).position(0); + mPosVertices = ByteBuffer.allocateDirect( + POS_VERTICES.length * FLOAT_SIZE_BYTES) + .order(ByteOrder.nativeOrder()).asFloatBuffer(); + mPosVertices.put(POS_VERTICES).position(0); + } + + public void tearDown() { + GLES20.glDeleteProgram(mProgram); + } + + public void updateTextureSize(int texWidth, int texHeight) { + mTexWidth = texWidth; + mTexHeight = texHeight; + computeOutputVertices(); + } + + public void updateViewSize(int viewWidth, int viewHeight) { + mViewWidth = viewWidth; + mViewHeight = viewHeight; + computeOutputVertices(); + } + + public void renderTexture(int texId) { + // Bind default FBO + GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0); + + // Use our shader program + GLES20.glUseProgram(mProgram); + GLToolbox.checkGlError("glUseProgram"); + + // Set viewport + GLES20.glViewport(0, 0, mViewWidth, mViewHeight); + GLToolbox.checkGlError("glViewport"); + + // Disable blending + GLES20.glDisable(GLES20.GL_BLEND); + + // Set the vertex attributes + GLES20.glVertexAttribPointer(mTexCoordHandle, 2, GLES20.GL_FLOAT, false, + 0, mTexVertices); + GLES20.glEnableVertexAttribArray(mTexCoordHandle); + GLES20.glVertexAttribPointer(mPosCoordHandle, 2, GLES20.GL_FLOAT, false, + 0, mPosVertices); + GLES20.glEnableVertexAttribArray(mPosCoordHandle); + GLToolbox.checkGlError("vertex attribute setup"); + + // Set the input texture + GLES20.glActiveTexture(GLES20.GL_TEXTURE0); + GLToolbox.checkGlError("glActiveTexture"); + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texId); + GLToolbox.checkGlError("glBindTexture"); + GLES20.glUniform1i(mTexSamplerHandle, 0); + + // Draw + GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); + } + + private void computeOutputVertices() { + if (mPosVertices != null) { + float imgAspectRatio = mTexWidth / (float)mTexHeight; + float viewAspectRatio = mViewWidth / (float)mViewHeight; + float relativeAspectRatio = viewAspectRatio / imgAspectRatio; + float x0, y0, x1, y1; + if (relativeAspectRatio > 1.0f) { + x0 = -1.0f / relativeAspectRatio; + y0 = -1.0f; + x1 = 1.0f / relativeAspectRatio; + y1 = 1.0f; + } else { + x0 = -1.0f; + y0 = -relativeAspectRatio; + x1 = 1.0f; + y1 = relativeAspectRatio; + } + float[] coords = new float[] { x0, y0, x1, y0, x0, y1, x1, y1 }; + mPosVertices.put(coords).position(0); + } + } + +} diff --git a/samples/browseable/MediaRecorder/AndroidManifest.xml b/samples/browseable/MediaRecorder/AndroidManifest.xml index 32f88f64f..539dc2c3d 100644 --- a/samples/browseable/MediaRecorder/AndroidManifest.xml +++ b/samples/browseable/MediaRecorder/AndroidManifest.xml @@ -22,9 +22,7 @@ android:versionCode="1" android:versionName="1.0"> - + diff --git a/samples/browseable/MediaRecorder/_index.jd b/samples/browseable/MediaRecorder/_index.jd index dac835aa8..28c55901f 100644 --- a/samples/browseable/MediaRecorder/_index.jd +++ b/samples/browseable/MediaRecorder/_index.jd @@ -2,6 +2,10 @@ page.tags="MediaRecorder" sample.group=Media @jd:body -

This sample demonstrates how to use the {@link android.media.MediaRecorder} -API to record video from a camera or camcorder, and display a preview of the -recording.

+

+ + This sample uses the camera/camcorder as the A/V source for the MediaRecorder API. + A TextureView is used as the camera preview which limits the code to API 14+. This + can be easily replaced with a SurfaceView to run on older devices. + +

diff --git a/samples/browseable/MediaRecorder/res/values-v21/template-styles.xml b/samples/browseable/MediaRecorder/res/values-v21/template-styles.xml new file mode 100644 index 000000000..134fcd9d3 --- /dev/null +++ b/samples/browseable/MediaRecorder/res/values-v21/template-styles.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/samples/browseable/MessagingService/res/values/colors.xml b/samples/browseable/MessagingService/res/values/colors.xml new file mode 100644 index 000000000..0e6825b0e --- /dev/null +++ b/samples/browseable/MessagingService/res/values/colors.xml @@ -0,0 +1,20 @@ + + + + #ff4092c3 + #ff241c99 + diff --git a/samples/browseable/MessagingService/res/values/dimens.xml b/samples/browseable/MessagingService/res/values/dimens.xml new file mode 100644 index 000000000..574a35d20 --- /dev/null +++ b/samples/browseable/MessagingService/res/values/dimens.xml @@ -0,0 +1,21 @@ + + + + + 16dp + 16dp + diff --git a/samples/browseable/MessagingService/res/values/strings.xml b/samples/browseable/MessagingService/res/values/strings.xml new file mode 100644 index 000000000..001b10eed --- /dev/null +++ b/samples/browseable/MessagingService/res/values/strings.xml @@ -0,0 +1,26 @@ + + + + Messaging Sample + Settings + Messaging Sample + Reply by Voice + Send 2 conversations with 1 message + Send 1 conversation with 1 message + Send 1 conversation with 3 messages + Clear Log + diff --git a/samples/browseable/MessagingService/res/values/styles.xml b/samples/browseable/MessagingService/res/values/styles.xml new file mode 100644 index 000000000..3f1a6af88 --- /dev/null +++ b/samples/browseable/MessagingService/res/values/styles.xml @@ -0,0 +1,20 @@ + + + + + diff --git a/samples/browseable/MessagingService/res/xml/automotive_app_desc.xml b/samples/browseable/MessagingService/res/xml/automotive_app_desc.xml new file mode 100644 index 000000000..9e9f1741f --- /dev/null +++ b/samples/browseable/MessagingService/res/xml/automotive_app_desc.xml @@ -0,0 +1,19 @@ + + + + + diff --git a/samples/browseable/MessagingService/src/com.example.android.messagingservice/Conversations.java b/samples/browseable/MessagingService/src/com.example.android.messagingservice/Conversations.java new file mode 100644 index 000000000..210e061f0 --- /dev/null +++ b/samples/browseable/MessagingService/src/com.example.android.messagingservice/Conversations.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.messagingservice; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + +/** + * A simple class that denotes unread conversations and messages. In a real world application, + * this would be replaced by a content provider that actually gets the unread messages to be + * shown to the user. + */ +public class Conversations { + + /** + * Set of strings used as messages by the sample. + */ + private static final String[] MESSAGES = new String[]{ + "Are you at home?", + "Can you give me a call?", + "Hey yt?", + "Don't forget to get some milk on your way back home", + "Is that project done?", + "Did you finish the Messaging app yet?" + }; + + /** + * Senders of the said messages. + */ + private static final String[] PARTICIPANTS = new String[]{ + "John Rambo", + "Han Solo", + "Rocky Balboa", + "Lara Croft" + }; + + static class Conversation { + + private final int conversationId; + + private final String participantName; + + /** + * A given conversation can have a single or multiple messages. + * Note that the messages are sorted from *newest* to *oldest* + */ + private final List messages; + + private final long timestamp; + + public Conversation(int conversationId, String participantName, + List messages) { + this.conversationId = conversationId; + this.participantName = participantName; + this.messages = messages == null ? Collections.emptyList() : messages; + this.timestamp = System.currentTimeMillis(); + } + + public int getConversationId() { + return conversationId; + } + + public String getParticipantName() { + return participantName; + } + + public List getMessages() { + return messages; + } + + public long getTimestamp() { + return timestamp; + } + + public String toString() { + return "[Conversation: conversationId=" + conversationId + + ", participantName=" + participantName + + ", messages=" + messages + + ", timestamp=" + timestamp + "]"; + } + } + + private Conversations() { + } + + public static Conversation[] getUnreadConversations(int howManyConversations, + int messagesPerConversation) { + Conversation[] conversations = new Conversation[howManyConversations]; + for (int i = 0; i < howManyConversations; i++) { + conversations[i] = new Conversation( + ThreadLocalRandom.current().nextInt(), + name(), makeMessages(messagesPerConversation)); + } + return conversations; + } + + private static List makeMessages(int messagesPerConversation) { + int maxLen = MESSAGES.length; + List messages = new ArrayList<>(messagesPerConversation); + for (int i = 0; i < messagesPerConversation; i++) { + messages.add(MESSAGES[ThreadLocalRandom.current().nextInt(0, maxLen)]); + } + return messages; + } + + private static String name() { + return PARTICIPANTS[ThreadLocalRandom.current().nextInt(0, PARTICIPANTS.length)]; + } +} diff --git a/samples/browseable/MessagingService/src/com.example.android.messagingservice/MainActivity.java b/samples/browseable/MessagingService/src/com.example.android.messagingservice/MainActivity.java new file mode 100644 index 000000000..e558a64a8 --- /dev/null +++ b/samples/browseable/MessagingService/src/com.example.android.messagingservice/MainActivity.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.messagingservice; + +import android.app.Activity; +import android.os.Bundle; + +public class MainActivity extends Activity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + if (savedInstanceState == null) { + getFragmentManager().beginTransaction() + .add(R.id.container, new MessagingFragment()) + .commit(); + } + } +} diff --git a/samples/browseable/MessagingService/src/com.example.android.messagingservice/MessageLogger.java b/samples/browseable/MessagingService/src/com.example.android.messagingservice/MessageLogger.java new file mode 100644 index 000000000..d1007b5ad --- /dev/null +++ b/samples/browseable/MessagingService/src/com.example.android.messagingservice/MessageLogger.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.messagingservice; + +import android.content.Context; +import android.content.SharedPreferences; + +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * A simple logger that uses shared preferences to log messages, their reads + * and replies. Don't use this in a real world application. This logger is only + * used for displaying the messages in the text view. + */ +public class MessageLogger { + + private static final String PREF_MESSAGE = "MESSAGE_LOGGER"; + private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + public static final String LOG_KEY = "message_data"; + public static final String LINE_BREAKS = "\n\n"; + + public static void logMessage(Context context, String message) { + SharedPreferences prefs = getPrefs(context); + message = DATE_FORMAT.format(new Date(System.currentTimeMillis())) + ": " + message; + prefs.edit() + .putString(LOG_KEY, prefs.getString(LOG_KEY, "") + LINE_BREAKS + message) + .apply(); + } + + public static SharedPreferences getPrefs(Context context) { + return context.getSharedPreferences(PREF_MESSAGE, Context.MODE_PRIVATE); + } + + public static String getAllMessages(Context context) { + return getPrefs(context).getString(LOG_KEY, ""); + } + + public static void clear(Context context) { + getPrefs(context).edit().remove(LOG_KEY).apply(); + } +} diff --git a/samples/browseable/MessagingService/src/com.example.android.messagingservice/MessageReadReceiver.java b/samples/browseable/MessagingService/src/com.example.android.messagingservice/MessageReadReceiver.java new file mode 100644 index 000000000..f28a3a778 --- /dev/null +++ b/samples/browseable/MessagingService/src/com.example.android.messagingservice/MessageReadReceiver.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.messagingservice; + +import android.app.NotificationManager; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.support.v4.app.NotificationManagerCompat; +import android.util.Log; + +public class MessageReadReceiver extends BroadcastReceiver { + private static final String TAG = MessageReadReceiver.class.getSimpleName(); + + private static final String CONVERSATION_ID = "conversation_id"; + + @Override + public void onReceive(Context context, Intent intent) { + Log.d(TAG, "onReceive"); + int conversationId = intent.getIntExtra(CONVERSATION_ID, -1); + if (conversationId != -1) { + Log.d(TAG, "Conversation " + conversationId + " was read"); + MessageLogger.logMessage(context, "Conversation " + conversationId + " was read."); + NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); + notificationManager.cancel(conversationId); + } + } +} diff --git a/samples/browseable/MessagingService/src/com.example.android.messagingservice/MessageReplyReceiver.java b/samples/browseable/MessagingService/src/com.example.android.messagingservice/MessageReplyReceiver.java new file mode 100644 index 000000000..0a3eba692 --- /dev/null +++ b/samples/browseable/MessagingService/src/com.example.android.messagingservice/MessageReplyReceiver.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.messagingservice; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.support.v4.app.RemoteInput; +import android.util.Log; + +/** + * A receiver that gets called when a reply is sent to a given conversationId + */ +public class MessageReplyReceiver extends BroadcastReceiver { + + private static final String TAG = MessageReplyReceiver.class.getSimpleName(); + + @Override + public void onReceive(Context context, Intent intent) { + if (MessagingService.REPLY_ACTION.equals(intent.getAction())) { + int conversationId = intent.getIntExtra(MessagingService.CONVERSATION_ID, -1); + CharSequence reply = getMessageText(intent); + if (conversationId != -1) { + Log.d(TAG, "Got reply (" + reply + ") for ConversationId " + conversationId); + MessageLogger.logMessage(context, "ConversationId: " + conversationId + + " received a reply: [" + reply + "]"); + } + } + } + + /** + * Get the message text from the intent. + * Note that you should call {@code RemoteInput#getResultsFromIntent(intent)} to process + * the RemoteInput. + */ + private CharSequence getMessageText(Intent intent) { + Bundle remoteInput = RemoteInput.getResultsFromIntent(intent); + if (remoteInput != null) { + return remoteInput.getCharSequence(MessagingService.EXTRA_VOICE_REPLY); + } + return null; + } +} diff --git a/samples/browseable/MessagingService/src/com.example.android.messagingservice/MessagingFragment.java b/samples/browseable/MessagingService/src/com.example.android.messagingservice/MessagingFragment.java new file mode 100644 index 000000000..f8efcc0c7 --- /dev/null +++ b/samples/browseable/MessagingService/src/com.example.android.messagingservice/MessagingFragment.java @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.messagingservice; + +import android.app.Fragment; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.os.IBinder; +import android.os.Message; +import android.os.Messenger; +import android.os.RemoteException; +import android.text.method.ScrollingMovementMethod; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.TextView; + +/** + * The main fragment that shows the buttons and the text view containing the log. + */ +public class MessagingFragment extends Fragment implements View.OnClickListener { + + private static final String TAG = MessagingFragment.class.getSimpleName(); + + private Button mSendSingleConversation; + private Button mSendTwoConversations; + private Button mSendConversationWithThreeMessages; + private TextView mDataPortView; + private Button mClearLogButton; + + private Messenger mService; + private boolean mBound; + + private ServiceConnection mConnection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName componentName, IBinder service) { + mService = new Messenger(service); + mBound = true; + setButtonsState(true); + } + + @Override + public void onServiceDisconnected(ComponentName componentName) { + mService = null; + mBound = false; + setButtonsState(false); + } + }; + + private SharedPreferences.OnSharedPreferenceChangeListener listener = + new SharedPreferences.OnSharedPreferenceChangeListener() { + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + if (MessageLogger.LOG_KEY.equals(key)) { + mDataPortView.setText(MessageLogger.getAllMessages(getActivity())); + } + } + }; + + public MessagingFragment() { + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View rootView = inflater.inflate(R.layout.fragment_message_me, container, false); + + mSendSingleConversation = (Button) rootView.findViewById(R.id.send_1_conversation); + mSendSingleConversation.setOnClickListener(this); + + mSendTwoConversations = (Button) rootView.findViewById(R.id.send_2_conversations); + mSendTwoConversations.setOnClickListener(this); + + mSendConversationWithThreeMessages = + (Button) rootView.findViewById(R.id.send_1_conversation_3_messages); + mSendConversationWithThreeMessages.setOnClickListener(this); + + mDataPortView = (TextView) rootView.findViewById(R.id.data_port); + mDataPortView.setMovementMethod(new ScrollingMovementMethod()); + + mClearLogButton = (Button) rootView.findViewById(R.id.clear); + mClearLogButton.setOnClickListener(this); + + setButtonsState(false); + + return rootView; + } + + @Override + public void onClick(View view) { + if (view == mSendSingleConversation) { + sendMsg(1, 1); + } else if (view == mSendTwoConversations) { + sendMsg(2, 1); + } else if (view == mSendConversationWithThreeMessages) { + sendMsg(1, 3); + } else if (view == mClearLogButton) { + MessageLogger.clear(getActivity()); + mDataPortView.setText(MessageLogger.getAllMessages(getActivity())); + } + } + + @Override + public void onStart() { + super.onStart(); + getActivity().bindService(new Intent(getActivity(), MessagingService.class), mConnection, + Context.BIND_AUTO_CREATE); + } + + @Override + public void onPause() { + super.onPause(); + MessageLogger.getPrefs(getActivity()).unregisterOnSharedPreferenceChangeListener(listener); + } + + @Override + public void onResume() { + super.onResume(); + mDataPortView.setText(MessageLogger.getAllMessages(getActivity())); + MessageLogger.getPrefs(getActivity()).registerOnSharedPreferenceChangeListener(listener); + } + + @Override + public void onStop() { + super.onStop(); + if (mBound) { + getActivity().unbindService(mConnection); + mBound = false; + } + } + + private void sendMsg(int howManyConversations, int messagesPerConversation) { + if (mBound) { + Message msg = Message.obtain(null, MessagingService.MSG_SEND_NOTIFICATION, + howManyConversations, messagesPerConversation); + try { + mService.send(msg); + } catch (RemoteException e) { + Log.e(TAG, "Error sending a message", e); + MessageLogger.logMessage(getActivity(), "Error occurred while sending a message."); + } + } + } + + private void setButtonsState(boolean enable) { + mSendSingleConversation.setEnabled(enable); + mSendTwoConversations.setEnabled(enable); + mSendConversationWithThreeMessages.setEnabled(enable); + } +} diff --git a/samples/browseable/MessagingService/src/com.example.android.messagingservice/MessagingService.java b/samples/browseable/MessagingService/src/com.example.android.messagingservice/MessagingService.java new file mode 100644 index 000000000..f980375d5 --- /dev/null +++ b/samples/browseable/MessagingService/src/com.example.android.messagingservice/MessagingService.java @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.messagingservice; + +import android.app.PendingIntent; +import android.app.Service; +import android.content.Intent; +import android.graphics.BitmapFactory; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.os.Messenger; +import android.preview.support.v4.app.NotificationCompat.CarExtender; +import android.preview.support.v4.app.NotificationCompat.CarExtender.UnreadConversation; +import android.support.v4.app.NotificationCompat; +import android.support.v4.app.NotificationManagerCompat; +import android.support.v4.app.RemoteInput; +import android.util.Log; + +import java.util.Iterator; + +public class MessagingService extends Service { + private static final String TAG = MessagingService.class.getSimpleName(); + + public static final String READ_ACTION = + "com.example.android.messagingservice.ACTION_MESSAGE_READ"; + public static final String REPLY_ACTION = + "com.example.android.messagingservice.ACTION_MESSAGE_REPLY"; + public static final String CONVERSATION_ID = "conversation_id"; + public static final String EXTRA_VOICE_REPLY = "extra_voice_reply"; + public static final int MSG_SEND_NOTIFICATION = 1; + public static final String EOL = "\n"; + + private NotificationManagerCompat mNotificationManager; + + private final Messenger mMessenger = new Messenger(new IncomingHandler()); + + /** + * Handler of incoming messages from clients. + */ + class IncomingHandler extends Handler { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_SEND_NOTIFICATION: + int howManyConversations = msg.arg1 <= 0 ? 1 : msg.arg1; + int messagesPerConv = msg.arg2 <= 0 ? 1 : msg.arg2; + sendNotification(howManyConversations, messagesPerConv); + break; + default: + super.handleMessage(msg); + } + } + } + + @Override + public void onCreate() { + Log.d(TAG, "onCreate"); + mNotificationManager = NotificationManagerCompat.from(getApplicationContext()); + } + + @Override + public IBinder onBind(Intent intent) { + Log.d(TAG, "onBind"); + return mMessenger.getBinder(); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + Log.d(TAG, "onStartCommand"); + return START_STICKY; + } + + @Override + public void onDestroy() { + super.onDestroy(); + Log.d(TAG, "onDestroy"); + } + + // Creates an intent that will be triggered when a message is marked as read. + private Intent getMessageReadIntent(int id) { + return new Intent() + .addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES) + .setAction(READ_ACTION) + .putExtra(CONVERSATION_ID, id); + } + + // Creates an Intent that will be triggered when a voice reply is received. + private Intent getMessageReplyIntent(int conversationId) { + return new Intent() + .addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES) + .setAction(REPLY_ACTION) + .putExtra(CONVERSATION_ID, conversationId); + } + + private void sendNotification(int howManyConversations, int messagesPerConversation) { + Conversations.Conversation[] conversations = Conversations.getUnreadConversations( + howManyConversations, messagesPerConversation); + for (Conversations.Conversation conv : conversations) { + sendNotificationForConversation(conv); + } + } + + private void sendNotificationForConversation(Conversations.Conversation conversation) { + // A pending Intent for reads + PendingIntent readPendingIntent = PendingIntent.getBroadcast(getApplicationContext(), + conversation.getConversationId(), + getMessageReadIntent(conversation.getConversationId()), + PendingIntent.FLAG_UPDATE_CURRENT); + + // Build a RemoteInput for receiving voice input in a Car Notification + RemoteInput remoteInput = new RemoteInput.Builder(EXTRA_VOICE_REPLY) + .setLabel(getApplicationContext().getString(R.string.notification_reply)) + .build(); + + // Building a Pending Intent for the reply action to trigger + PendingIntent replyIntent = PendingIntent.getBroadcast(getApplicationContext(), + conversation.getConversationId(), + getMessageReplyIntent(conversation.getConversationId()), + PendingIntent.FLAG_UPDATE_CURRENT); + + // Create the UnreadConversation and populate it with the participant name, + // read and reply intents. + UnreadConversation.Builder unreadConvBuilder = + new UnreadConversation.Builder(conversation.getParticipantName()) + .setLatestTimestamp(conversation.getTimestamp()) + .setReadPendingIntent(readPendingIntent) + .setReplyAction(replyIntent, remoteInput); + + // Note: Add messages from oldest to newest to the UnreadConversation.Builder + StringBuilder messageForNotification = new StringBuilder(); + for (Iterator messages = conversation.getMessages().iterator(); + messages.hasNext(); ) { + String message = messages.next(); + unreadConvBuilder.addMessage(message); + messageForNotification.append(message); + if (messages.hasNext()) { + messageForNotification.append(EOL); + } + } + + NotificationCompat.Builder builder = new NotificationCompat.Builder(getApplicationContext()) + .setSmallIcon(R.drawable.notification_icon) + .setLargeIcon(BitmapFactory.decodeResource( + getApplicationContext().getResources(), R.drawable.android_contact)) + .setContentText(messageForNotification.toString()) + .setWhen(conversation.getTimestamp()) + .setContentTitle(conversation.getParticipantName()) + .setContentIntent(readPendingIntent) + .extend(new CarExtender() + .setUnreadConversation(unreadConvBuilder.build()) + .setColor(getApplicationContext() + .getResources().getColor(R.color.default_color_light))); + + MessageLogger.logMessage(getApplicationContext(), "Sending notification " + + conversation.getConversationId() + " conversation: " + conversation); + + mNotificationManager.notify(conversation.getConversationId(), builder.build()); + } +} diff --git a/samples/browseable/DelayedConfirmation/Shared/res/values/strings.xml b/samples/browseable/NavigationDrawer/res/values/template-attrs.xml similarity index 92% rename from samples/browseable/DelayedConfirmation/Shared/res/values/strings.xml rename to samples/browseable/NavigationDrawer/res/values/template-attrs.xml index 0f2bb9075..442ed7781 100644 --- a/samples/browseable/DelayedConfirmation/Shared/res/values/strings.xml +++ b/samples/browseable/NavigationDrawer/res/values/template-attrs.xml @@ -1,18 +1,15 @@ + - Shared - + \ No newline at end of file diff --git a/samples/browseable/NetworkConnect/AndroidManifest.xml b/samples/browseable/NetworkConnect/AndroidManifest.xml index 1ae29df9a..00ce7f3d0 100644 --- a/samples/browseable/NetworkConnect/AndroidManifest.xml +++ b/samples/browseable/NetworkConnect/AndroidManifest.xml @@ -23,7 +23,7 @@ android:versionCode="1" android:versionName="1.0"> - + diff --git a/samples/browseable/NetworkConnect/_index.jd b/samples/browseable/NetworkConnect/_index.jd index eaac88496..7d67dd3dd 100644 --- a/samples/browseable/NetworkConnect/_index.jd +++ b/samples/browseable/NetworkConnect/_index.jd @@ -1,10 +1,10 @@ - - - page.tags="NetworkConnect" sample.group=Connectivity @jd:body -

This sample demonstrates how to connect to the network and fetch raw HTML. -The sample uses {@link android.os.AsyncTask} to perform the fetch on a -background thread.

+

+ + This sample demonstrates how to connect to the network and fetch raw HTML using + HttpURLConnection. AsyncTask is used to perform the fetch on a background thread. + +

diff --git a/samples/browseable/NetworkConnect/res/values-v21/template-styles.xml b/samples/browseable/NetworkConnect/res/values-v21/template-styles.xml new file mode 100644 index 000000000..134fcd9d3 --- /dev/null +++ b/samples/browseable/NetworkConnect/res/values-v21/template-styles.xml @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/samples/browseable/PdfRendererBasic/res/values-v11/template-styles.xml b/samples/browseable/PdfRendererBasic/res/values-v11/template-styles.xml new file mode 100644 index 000000000..8c1ea66f2 --- /dev/null +++ b/samples/browseable/PdfRendererBasic/res/values-v11/template-styles.xml @@ -0,0 +1,22 @@ + + + + + + + + + + diff --git a/samples/browseable/PdfRendererBasic/src/com.example.android.pdfrendererbasic/MainActivity.java b/samples/browseable/PdfRendererBasic/src/com.example.android.pdfrendererbasic/MainActivity.java new file mode 100644 index 000000000..6b7e8b454 --- /dev/null +++ b/samples/browseable/PdfRendererBasic/src/com.example.android.pdfrendererbasic/MainActivity.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.pdfrendererbasic; + +import android.app.Activity; +import android.app.AlertDialog; +import android.os.Bundle; +import android.view.Menu; +import android.view.MenuItem; + +public class MainActivity extends Activity { + + public static final String FRAGMENT_PDF_RENDERER_BASIC = "pdf_renderer_basic"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main_real); + if (savedInstanceState == null) { + getFragmentManager().beginTransaction() + .add(R.id.container, new PdfRendererBasicFragment(), + FRAGMENT_PDF_RENDERER_BASIC) + .commit(); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.main, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.action_info: + new AlertDialog.Builder(this) + .setMessage(R.string.intro_message) + .setPositiveButton(android.R.string.ok, null) + .show(); + return true; + } + return super.onOptionsItemSelected(item); + } +} diff --git a/samples/browseable/PdfRendererBasic/src/com.example.android.pdfrendererbasic/PdfRendererBasicFragment.java b/samples/browseable/PdfRendererBasic/src/com.example.android.pdfrendererbasic/PdfRendererBasicFragment.java new file mode 100644 index 000000000..e413c6599 --- /dev/null +++ b/samples/browseable/PdfRendererBasic/src/com.example.android.pdfrendererbasic/PdfRendererBasicFragment.java @@ -0,0 +1,221 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.pdfrendererbasic; + +import android.app.Activity; +import android.app.Fragment; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.pdf.PdfRenderer; +import android.os.Bundle; +import android.os.ParcelFileDescriptor; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.Toast; + +import java.io.IOException; + +/** + * This fragment has a big {@ImageView} that shows PDF pages, and 2 {@link android.widget.Button}s to move between + * pages. We use a {@link android.graphics.pdf.PdfRenderer} to render PDF pages as {@link android.graphics.Bitmap}s. + */ +public class PdfRendererBasicFragment extends Fragment implements View.OnClickListener { + + /** + * Key string for saving the state of current page index. + */ + private static final String STATE_CURRENT_PAGE_INDEX = "current_page_index"; + + /** + * File descriptor of the PDF. + */ + private ParcelFileDescriptor mFileDescriptor; + + /** + * {@link android.graphics.pdf.PdfRenderer} to render the PDF. + */ + private PdfRenderer mPdfRenderer; + + /** + * Page that is currently shown on the screen. + */ + private PdfRenderer.Page mCurrentPage; + + /** + * {@link android.widget.ImageView} that shows a PDF page as a {@link android.graphics.Bitmap} + */ + private ImageView mImageView; + + /** + * {@link android.widget.Button} to move to the previous page. + */ + private Button mButtonPrevious; + + /** + * {@link android.widget.Button} to move to the next page. + */ + private Button mButtonNext; + + public PdfRendererBasicFragment() { + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_pdf_renderer_basic, container, false); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + // Retain view references. + mImageView = (ImageView) view.findViewById(R.id.image); + mButtonPrevious = (Button) view.findViewById(R.id.previous); + mButtonNext = (Button) view.findViewById(R.id.next); + // Bind events. + mButtonPrevious.setOnClickListener(this); + mButtonNext.setOnClickListener(this); + // Show the first page by default. + int index = 0; + // If there is a savedInstanceState (screen orientations, etc.), we restore the page index. + if (null != savedInstanceState) { + index = savedInstanceState.getInt(STATE_CURRENT_PAGE_INDEX, 0); + } + showPage(index); + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + try { + openRenderer(activity); + } catch (IOException e) { + e.printStackTrace(); + Toast.makeText(activity, "Error! " + e.getMessage(), Toast.LENGTH_SHORT).show(); + activity.finish(); + } + } + + @Override + public void onDetach() { + try { + closeRenderer(); + } catch (IOException e) { + e.printStackTrace(); + } + super.onDetach(); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + if (null != mCurrentPage) { + outState.putInt(STATE_CURRENT_PAGE_INDEX, mCurrentPage.getIndex()); + } + } + + /** + * Sets up a {@link android.graphics.pdf.PdfRenderer} and related resources. + */ + private void openRenderer(Context context) throws IOException { + // In this sample, we read a PDF from the assets directory. + mFileDescriptor = context.getAssets().openFd("sample.pdf").getParcelFileDescriptor(); + // This is the PdfRenderer we use to render the PDF. + mPdfRenderer = new PdfRenderer(mFileDescriptor); + } + + /** + * Closes the {@link android.graphics.pdf.PdfRenderer} and related resources. + * + * @throws java.io.IOException When the PDF file cannot be closed. + */ + private void closeRenderer() throws IOException { + if (null != mCurrentPage) { + mCurrentPage.close(); + } + mPdfRenderer.close(); + mFileDescriptor.close(); + } + + /** + * Shows the specified page of PDF to the screen. + * + * @param index The page index. + */ + private void showPage(int index) { + if (mPdfRenderer.getPageCount() <= index) { + return; + } + // Make sure to close the current page before opening another one. + if (null != mCurrentPage) { + mCurrentPage.close(); + } + // Use `openPage` to open a specific page in PDF. + mCurrentPage = mPdfRenderer.openPage(index); + // Important: the destination bitmap must be ARGB (not RGB). + Bitmap bitmap = Bitmap.createBitmap(mCurrentPage.getWidth(), mCurrentPage.getHeight(), + Bitmap.Config.ARGB_8888); + // Here, we render the page onto the Bitmap. + // To render a portion of the page, use the second and third parameter. Pass nulls to get + // the default result. + // Pass either RENDER_MODE_FOR_DISPLAY or RENDER_MODE_FOR_PRINT for the last parameter. + mCurrentPage.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY); + // We are ready to show the Bitmap to user. + mImageView.setImageBitmap(bitmap); + updateUi(); + } + + /** + * Updates the state of 2 control buttons in response to the current page index. + */ + private void updateUi() { + int index = mCurrentPage.getIndex(); + int pageCount = mPdfRenderer.getPageCount(); + mButtonPrevious.setEnabled(0 != index); + mButtonNext.setEnabled(index + 1 < pageCount); + getActivity().setTitle(getString(R.string.app_name_with_index, index + 1, pageCount)); + } + + /** + * Gets the number of pages in the PDF. This method is marked as public for testing. + * + * @return The number of pages. + */ + public int getPageCount() { + return mPdfRenderer.getPageCount(); + } + + @Override + public void onClick(View view) { + switch (view.getId()) { + case R.id.previous: { + // Move to the previous page + showPage(mCurrentPage.getIndex() - 1); + break; + } + case R.id.next: { + // Move to the next page + showPage(mCurrentPage.getIndex() + 1); + break; + } + } + } + +} diff --git a/samples/browseable/Quiz/Application/AndroidManifest.xml b/samples/browseable/Quiz/Application/AndroidManifest.xml index 40e36027e..ce7213542 100644 --- a/samples/browseable/Quiz/Application/AndroidManifest.xml +++ b/samples/browseable/Quiz/Application/AndroidManifest.xml @@ -15,7 +15,7 @@ --> + package="com.example.android.wearable.quiz" > @@ -29,7 +29,7 @@ android:value="@integer/google_play_services_version" /> diff --git a/samples/browseable/Quiz/Application/res/values-v21/template-styles.xml b/samples/browseable/Quiz/Application/res/values-v21/template-styles.xml new file mode 100644 index 000000000..134fcd9d3 --- /dev/null +++ b/samples/browseable/Quiz/Application/res/values-v21/template-styles.xml @@ -0,0 +1,22 @@ + + + + + +